Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
35cbf58
example(iaas): scaffold the restricted egress example
Oct 7, 2026
968a872
example(iaas): add network area, project and network
Oct 7, 2026
8e1c026
example(iaas): add security groups and the egress allow-list
Oct 7, 2026
5c25454
example(iaas): remove the default egress rules of the target
Oct 7, 2026
e060902
example(iaas): add cloud-init for the jump host and the probe
Oct 7, 2026
08568a1
example(iaas): add the jump host and the target server
Oct 7, 2026
b7325d3
example(iaas): add outputs with SSH and verify commands
Oct 7, 2026
7285e62
example(iaas): add tfvars example and reject its placeholders
Oct 7, 2026
a8a3fac
docs(iaas): document the restricted egress example
Oct 7, 2026
d8756b3
fix(iaas): read the rules from the list endpoint of the IaaS API
Oct 15, 2026
6ec4f37
docs(iaas): describe the rule listing of the removal script
Oct 15, 2026
c0472cd
fix(iaas): limit nameservers to three distinct addresses
Oct 15, 2026
08b4831
docs(iaas): say that an egress entry becomes one rule per range
Oct 15, 2026
c76babd
docs(iaas): drop the completeness claim about egress_rules
Oct 15, 2026
80792df
docs(iaas): name the provider as the one that refuses the delete
Oct 15, 2026
9e4e438
docs(iaas): note that the key pair name is unique per account
Oct 15, 2026
115c614
docs(iaas): state the ingress rules Terraform adds to each group
Oct 15, 2026
2b942c3
docs(iaas): say from which point on the probe logs connections
Oct 15, 2026
2d26848
docs(iaas): cite the sources of the metadata service address
Oct 15, 2026
8592e36
docs(iaas): link the portal guide for enabling Run Command
Oct 15, 2026
1ced2ef
example(iaas): add a check for a destination outside the allow-list
Oct 15, 2026
5337e84
docs(iaas): quote the concepts page on same-group traffic
Oct 15, 2026
25eb98e
docs(iaas): tell a blocked connection from a failed DNS lookup
Oct 15, 2026
3c8bbe6
example(iaas): give a service account the role run-command.editor
Oct 15, 2026
19c71d2
docs(iaas): explain who may send Run Command and how to switch to it
Oct 15, 2026
229f13c
docs(iaas): list the Run Command account among the outputs
Oct 15, 2026
8d6bd56
fix(iaas): allow DHCP and drop DNS and HTTPS from the egress default
Oct 15, 2026
e1c79c2
docs(iaas): explain how admin_cidr can miss the address STACKIT sees
Oct 15, 2026
11d8463
docs(iaas): check the data source without SSH and tighten the wording
Oct 15, 2026
3b1e0b8
refactor(iaas): drop the egress probe and the open mode
Oct 16, 2026
254f8f0
docs(iaas): align comments and README with the trimmed example
Oct 16, 2026
ab30f3c
example(iaas): reject the remaining placeholders of the tfvars example
Oct 16, 2026
e586a17
docs(iaas): shorten the README of the egress example
Oct 16, 2026
3b7e260
refactor(iaas): delete the default egress rules inline, without scripts/
Oct 16, 2026
d017c52
fix(iaas): bound the curl calls and accept an already deleted rule
Oct 16, 2026
9aebc88
docs(iaas): add the region to CLI calls and close gaps in the README
Oct 16, 2026
59e1fea
refactor(iaas): move the jump host's cloud-config into a template
Oct 19, 2026
c7c58d1
refactor(iaas): load the default-rule deletion from a shell script
Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ Fetch only the files relevant to the task. A typical example contains
Upload a custom VM image to STACKIT using Terraform
- **`iaas-k3s-self-managed`** `[iaas, k3s, kubernetes, self-managed]`
Deploys a 6-node k3s + Cilium cluster on STACKIT VMs and validates: L4 NLB, L7 ALB, CSI block storage, Cilium NetworkPolicy, and egress connectivity
- **`iaas-server-agent-restricted-egress`** `[iaas, compute, networking, sna, security-group, egress, cloud-init, server-agent, jump-host, ssh]`
This example deploys a Linux VM, the target. Its security group allows outbound traffic only to an allow-list. The platform installs the STACKIT Server Agent on the target. A jump host is the only way to reach the target over SSH. The example adds the two egress rules that cloud-init, the Server Agent and Run Command need: DHCP and the metadata service. Every new security group also carries two default egress rules that allow all outbound traffic. The STACKIT provider cannot remove them, so Terraform deletes them through the IaaS API
- **`iaas-volume-encryption`** `[iaas, volume, encryption, kms, block-storage]`
KMS & IaaS Resources to deploy a encrypted Block Storage Volume
- **`iaas-windows-byol-stackit-migration`** `[iaas, windows, byol, migration, compute]`
Expand Down
25 changes: 25 additions & 0 deletions examples/iaas-server-agent-restricted-egress/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions examples/iaas-server-agent-restricted-egress/010-provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

terraform {
# Ephemeral resources, used for the access token in
# 070-default-egress-rules.tf, exist since Terraform 1.10.
required_version = ">= 1.10.0"

required_providers {
stackit = {
source = "stackitcloud/stackit"
version = ">= 0.118.0"
}
}
}

provider "stackit" {
default_region = var.stackit_region
service_account_key_path = var.stackit_service_account_key_path
# Required by the ephemeral resource stackit_access_token in
# 070-default-egress-rules.tf.
enable_beta_resources = true
# Required by stackit_authorization_project_role_assignment in
# 045-run-command-access.tf.
experiments = ["iam"]
}
260 changes: 260 additions & 0 deletions examples/iaas-server-agent-restricted-egress/020-variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

variable "stackit_org_id" {
description = "The STACKIT organization ID in which the network area is created."
type = string

validation {
condition = can(regex("^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$", var.stackit_org_id))
error_message = "stackit_org_id must be the UUID of an organization, not the placeholder from terraform.tfvars.example."
}
}

variable "stackit_parent_container_id" {
description = "Existing folder ID the project is created under, or the organization ID to create it directly under the organization. This example does not create the folder."
type = string

validation {
condition = !startswith(var.stackit_parent_container_id, "xxxxxxxx-")
error_message = "stackit_parent_container_id is still the placeholder from terraform.tfvars.example."
}
}

variable "stackit_admin_email" {
description = "An existing STACKIT user that becomes owner of the created project. A non-existent address fails with 409 \"subject is not found\"."
type = string

validation {
condition = !endswith(lower(var.stackit_admin_email), "@example.com")
error_message = "stackit_admin_email is still the placeholder from terraform.tfvars.example."
}
}

variable "stackit_region" {
description = "The STACKIT region."
type = string
default = "eu01"
}

variable "stackit_service_account_key_path" {
description = "Path to the service account key file. Unset falls back to the STACKIT_SERVICE_ACCOUNT_KEY_PATH environment variable, then to that entry in the credentials file ($HOME/.stackit/credentials.json, or STACKIT_CREDENTIALS_PATH)."
type = string
default = null
}

# --- naming --------------------------------------------------------------

variable "project_name" {
description = "Display name of the project this example creates."
type = string
default = "server-agent-egress"
}

variable "network_area_name" {
description = "Name of the STACKIT Network Area this example creates."
type = string
default = "server-agent-egress"
}

variable "name_prefix" {
description = "Prefix for the names of the network, the security groups, the network interfaces, the key pair and the two machines. The key pair belongs to the service account that runs Terraform, not to the project. Its name is unique for that account, so a second deployment with the same service account needs another prefix."
type = string
default = "agent-egress"
}

# --- addressing ----------------------------------------------------------

variable "network_area_range" {
description = "IPv4 range of the network area."
type = string
default = "10.80.0.0/16"
}

variable "network_area_transfer_network" {
description = "Transfer network of the network area. It must not overlap with network_area_range."
type = string
default = "172.30.0.0/16"
}

variable "network_prefix" {
description = "Prefix of the network that holds both machines. It must sit inside network_area_range."
type = string
default = "10.80.1.0/24"
}

variable "nameservers" {
description = "One to three IPv4 nameservers of the network. The egress rules with ip_range = \"nameservers\" allow DNS to exactly these addresses."
type = list(string)
default = ["1.1.1.1", "9.9.9.9"]

# The IaaS API takes at most three nameservers. Every address becomes part
# of a for_each key in 060-security-groups.tf, so no address repeats.
validation {
condition = (
length(var.nameservers) >= 1 && length(var.nameservers) <= 3 &&
length(distinct(var.nameservers)) == length(var.nameservers) &&
alltrue([for ns in var.nameservers : !strcontains(ns, ":") && can(cidrhost("${ns}/32", 0))])
)
error_message = "nameservers needs one to three distinct entries, and every entry must be an IPv4 address without a prefix length."
}
}

# --- access --------------------------------------------------------------

variable "admin_cidr" {
description = "IPv4 source range that reaches the jump host on TCP 2222, for example your public IP with a /32 suffix. It must hold the address that STACKIT sees. An internet line with several public IPv4 addresses can show STACKIT a different address from the one an IP lookup service shows."
type = string

validation {
condition = !strcontains(var.admin_cidr, ":") && can(cidrhost(var.admin_cidr, 0))
error_message = "admin_cidr must be an IPv4 range in CIDR notation, for example your public IPv4 address with a /32 suffix."
}

validation {
condition = !startswith(var.admin_cidr, "198.51.100.")
error_message = "admin_cidr is still the placeholder from terraform.tfvars.example."
}
}

# --- Run Command ---------------------------------------------------------

variable "run_command_service_account_email" {
description = "Email of an existing service account that sends Run Command to the target. Terraform gives it the role run-command.editor on the project. Unset, Terraform creates a service account in the project and gives the role to that one."
type = string
default = null

# The provider rejects a subject with upper-case letters, but only at plan.
validation {
condition = var.run_command_service_account_email == null || can(regex("^[a-z0-9._-]+@[a-z0-9.-]+$", var.run_command_service_account_email))
error_message = "run_command_service_account_email must be a service account email in lower case, for example name-1234567@sa.stackit.cloud."
}
}

variable "run_command_service_account_name" {
description = "Name of the service account Terraform creates when run_command_service_account_email is unset. STACKIT appends a random suffix to form the email."
type = string
default = "run-command"

# The provider checks these rules only at plan.
validation {
condition = length(var.run_command_service_account_name) <= 20 && can(regex("^[a-z](-?[a-z0-9]+)*$", var.run_command_service_account_name))
error_message = "run_command_service_account_name has at most 20 characters, starts with a lower-case letter and holds lower-case letters, digits and single dashes, but no dash at the end."
}
}

# --- outbound traffic of the target --------------------------------------

variable "egress_rules" {
description = "Outbound allow-list of the target's security group. Each entry becomes one IPv4 egress rule per address range. ip_range takes a CIDR or one of two names: \"nameservers\" stands for every address in var.nameservers, \"stackit\" for every IPv4 range in data.stackit_public_ip_ranges. Leave port_min and port_max unset for a protocol without ports, such as icmp. The default holds the two rules cloud-init, the Server Agent and Run Command need: DHCP and the metadata service."
type = map(object({
description = string
protocol = string
port_min = optional(number)
port_max = optional(number)
ip_range = string
}))
default = {
dhcp = {
description = "DHCP: lease and renewal of the IPv4 address of the interface"
protocol = "udp"
port_min = 67
port_max = 67
ip_range = "0.0.0.0/0"
}
metadata = {
description = "Metadata service: cloud-init data, Server Agent package, traffic of the Server Agent and the monitoring agent"
protocol = "tcp"
port_min = 80
port_max = 80
ip_range = "169.254.169.254/32"
}
}

validation {
condition = alltrue([
for rule in values(var.egress_rules) :
contains(["nameservers", "stackit"], rule.ip_range) || (!strcontains(rule.ip_range, ":") && can(cidrhost(rule.ip_range, 0)))
])
error_message = "ip_range of every egress rule must be an IPv4 range in CIDR notation, \"nameservers\" or \"stackit\"."
}

validation {
condition = alltrue([
for rule in values(var.egress_rules) :
length(rule.description) >= 1 && length(rule.description) <= 127
])
error_message = "description of every egress rule must have 1 to 127 characters. The STACKIT provider rejects longer rule descriptions."
}

validation {
condition = alltrue([
for rule in values(var.egress_rules) :
(rule.port_min == null) == (rule.port_max == null)
])
error_message = "Every egress rule sets both port_min and port_max, or neither."
}
}

# --- machines ------------------------------------------------------------

variable "ssh_public_key" {
description = "Public SSH key installed on both machines for the default user of the image. Never the private key."
type = string

validation {
condition = !strcontains(var.ssh_public_key, "AAAA...")
error_message = "ssh_public_key is still the placeholder from terraform.tfvars.example."
}
}

variable "ssh_user" {
description = "User that receives ssh_public_key. STACKIT images create one default user per distribution; on Ubuntu it is \"ubuntu\". The outputs use the name in their SSH commands."
type = string
default = "ubuntu"
}

variable "image_id" {
description = "Image UUID of both machines. The user data of the jump host expects the public image \"Ubuntu 24.04\" (amd64), whose main sshd configuration reads /etc/ssh/sshd_config.d and sets no port. Look the ID up with: stackit image list --all --project-id <existing project>"
type = string

validation {
condition = can(regex("^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$", var.image_id))
error_message = "image_id must be the UUID of an image, not the placeholder from terraform.tfvars.example."
}
}

variable "availability_zone" {
description = "Availability zone of both machines."
type = string
default = "eu01-1"
}

variable "jump_machine_type" {
description = "Machine type of the jump host. t3i.1 has 1 vCPU and 1 GB RAM."
type = string
default = "t3i.1"
}

variable "target_machine_type" {
description = "Machine type of the target. c3i.1 has 1 vCPU and 2 GB RAM."
type = string
default = "c3i.1"
}

variable "boot_volume_size" {
description = "Size of each boot volume in GB."
type = number
default = 20
}
31 changes: 31 additions & 0 deletions examples/iaas-server-agent-restricted-egress/030-network-area.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

resource "stackit_network_area" "this" {
organization_id = var.stackit_org_id
name = var.network_area_name
}

# The regional configuration holds the address ranges of the network area.
# Networks and projects of the area need it, so both depend on it.
resource "stackit_network_area_region" "this" {
organization_id = var.stackit_org_id
network_area_id = stackit_network_area.this.network_area_id

ipv4 = {
transfer_network = var.network_area_transfer_network
network_ranges = [{ prefix = var.network_area_range }]
default_nameservers = var.nameservers
}
}
Loading
Loading