diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 0b4d3e3..abda7a2 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,3 +1,17 @@ +# 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. + version: 2 updates: - package-ecosystem: "github-actions" diff --git a/.github/workflows/default-ci.yaml b/.github/workflows/default-ci.yaml index 7f9f84d..e2dacc0 100644 --- a/.github/workflows/default-ci.yaml +++ b/.github/workflows/default-ci.yaml @@ -1,3 +1,17 @@ +# 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. + name: "Default CI" on: diff --git a/.github/workflows/github-mirror-ci.yaml b/.github/workflows/github-mirror-ci.yaml index de61ea9..01ef428 100644 --- a/.github/workflows/github-mirror-ci.yaml +++ b/.github/workflows/github-mirror-ci.yaml @@ -1,3 +1,17 @@ +# 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. + name: "Mirror to Public GitHub" on: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b00eab..6c7483b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,17 @@ +# 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. + # .pre-commit-config.yaml # To use this file, install pre-commit (https://pre-commit.com): # pip install pre-commit @@ -52,5 +66,5 @@ repos: # Requires `addlicense` to be installed locally (go install github.com/google/addlicense@latest) entry: addlicense -c "Schwarz Digits Cloud GmbH & Co. KG" -l apache language: system - types_or: [terraform, python, go, javascript] + types_or: [terraform, python, go, javascript, yaml, json] pass_filenames: true diff --git a/examples/alb-tls-examples/MAINTAINERS.md b/examples/alb-tls-examples/MAINTAINERS.md new file mode 100644 index 0000000..345a653 --- /dev/null +++ b/examples/alb-tls-examples/MAINTAINERS.md @@ -0,0 +1,9 @@ +# Maintainers + +General maintainers: + +- Sven Schmidt (sven.schmidt@digits.schwarz) + +This example is actively maintained. The owner is responsible for reviewing and updating dependencies and functionalities on a monthly basis. +For questions, issues, or feature requests, please email general maintainers. +Please include the BP name and version in your request. We will track your request as an issue. diff --git a/examples/alb-tls-examples/README.md b/examples/alb-tls-examples/README.md new file mode 100644 index 0000000..fb759fa --- /dev/null +++ b/examples/alb-tls-examples/README.md @@ -0,0 +1,175 @@ +# alb-tls-examples + +A collection of STACKIT Application Load Balancer (ALB) showcases with different TLS strategies — from self-signed to Let's Encrypt, from a single VM to Kubernetes. + +Each subfolder is a self-contained, runnable Terraform showcase with its own README, state, and variables. The showcases are intentionally independent — no shared state, no shared modules. + +--- + +## Overview + +``` +alb-tls-examples/ +│ +├── vm-alb-self-signed-cert/ ← Starting point: 1 VM + ALB + Self-Signed Cert (Terraform) +├── vm-alb-certbot-letsencrypt/ ← Production: VM + ALB + Let's Encrypt via certbot + ACME DNS-01 +└── alb-k8s/ ← Kubernetes: SKE + cert-manager + Let's Encrypt +``` + +--- + +## Showcase Comparison + +| | `vm-alb-self-signed-cert` | `vm-alb-certbot-letsencrypt` | `alb-k8s` | +| ------------------------- | ---------------------------- | ----------------------------- | ----------------------------- | +| **Goal** | Getting started / quickstart | Production-grade | Kubernetes path | +| **Certificate** | Self-signed (Terraform) | Let's Encrypt (certbot) | Let's Encrypt (cert-manager) | +| **Backend** | 1 VM + Docker nginx | 1 VM + Docker nginx | SKE cluster + nginx Pod | +| **Terraform** | Yes | Yes (Phase 1) | Yes | +| **Docker** | Yes (nginx) | Yes (nginx + certbot) | No | +| **Kubernetes** | No | No | Yes (SKE) | +| **Auto-renewal** | No (re-apply) | Yes (cron on VM) | Yes (cert-manager) | +| **External dependencies** | None | Let's Encrypt, DNS delegation | Let's Encrypt, DNS delegation | +| **Time to HTTPS** | ~5 min | ~20 min | ~20 min | + +--- + +## Learning Path + +**1. [`vm-alb-self-signed-cert/`](vm-alb-self-signed-cert/README.md)** + +- How does a STACKIT ALB work? +- How is a TLS certificate attached to the ALB? +- How does the ALB terminate HTTPS and forward to a backend? + +**2. [`vm-alb-certbot-letsencrypt/`](vm-alb-certbot-letsencrypt/README.md)** + +- How do I replace a self-signed cert with a trusted one? +- How does the ACME DNS-01 challenge work with STACKIT DNS? +- How does automatic certificate renewal work via certbot in Docker? + +**3. [`alb-k8s/`](alb-k8s/README.md)** + +- How does the same work on Kubernetes? +- How do cert-manager, Ingress, and STACKIT SKE interact? + +--- + +## Common Prerequisites + +| Requirement | Details | +| ----------------------- | -------------------------------------------------------- | +| STACKIT account | Access to the STACKIT Portal | +| Terraform | >= 1.5.7 (recommended: use `tfenv`) | +| STACKIT CLI | For image UUIDs, project IDs, debugging | +| SSH key pair | Ed25519 or RSA — only the public key goes into Terraform | +| STACKIT service account | JSON key with the roles required for the showcase | +| STACKIT Object Storage | For the S3-compatible Terraform remote state backend | + +### Create a service account + +```bash +stackit iam service-account create \ + --project-id \ + --name "tf-workshop-sa" + +mkdir -p keys +stackit iam service-account key create \ + --project-id \ + --service-account-email \ + --output-format json > keys/sa-key.json +``` + +### Useful CLI commands + +```bash +# List available Debian 12 images +stackit image list --all --project-id + +# List available machine types +stackit server machine-type list --project-id + +# List projects +stackit project list + +# Find your egress IP (for admin_cidr) +curl -s https://ifconfig.schwarz +``` + +--- + +## Showcase Descriptions + +### [`vm-alb-self-signed-cert/`](vm-alb-self-signed-cert/README.md) + +**Introductory showcase — recommended starting point.** + +A single Debian 12 VM with Docker and `nginx:alpine` behind a STACKIT Application Load Balancer. The TLS certificate is self-signed and fully managed by Terraform — no external tools, no DNS delegation required. + +``` +Internet → ALB (HTTPS :443, Self-Signed Cert) → VM (Docker nginx :80) +``` + +- Terraform generates RSA key + self-signed cert (`hashicorp/tls` provider) +- Terraform uploads the certificate via `stackit_alb_certificate` +- The ALB terminates TLS, the VM receives plain HTTP +- STACKIT DNS Zone + A-Record created as part of the same apply + +**When to use:** When you want to understand the ALB + TLS mechanism without extra complexity. + +--- + +### [`vm-alb-certbot-letsencrypt/`](vm-alb-certbot-letsencrypt/README.md) + +**Production showcase with automatic certificate renewal.** + +Same infrastructure as `vm-alb-self-signed-cert`, but with a full ACME pipeline on the VM. Terraform provisions the ALB without a certificate; a certbot Docker container then issues and renews Let's Encrypt certificates via DNS-01 challenge. + +``` +Phase 1 (Terraform): VM + ALB (HTTP + target) + DNS Zone +Phase 2 (certbot): ACME DNS-01 → Let's Encrypt Cert → ALB HTTPS listener +Phase 3+ (cron): Monthly automatic renewal +``` + +- Requires a delegated DNS zone (set NS records at your registrar) +- `lifecycle { ignore_changes = [listeners] }` allows certbot to update the ALB outside of Terraform + +**When to use:** When you want a production-realistic, fully automated certificate lifecycle. + +--- + +### [`alb-k8s/`](alb-k8s/README.md) + +**Kubernetes showcase with STACKIT SKE and cert-manager.** + +Terraform creates a STACKIT Kubernetes Engine (SKE) cluster. nginx is deployed as a Kubernetes Deployment, the STACKIT NLB acts as the ingress point. cert-manager handles automatic certificate management via Let's Encrypt DNS-01 with STACKIT DNS. + +``` +Internet → STACKIT NLB (L4, auto) → Traefik IC (L7, in-cluster) → nginx Pod + cert-manager (Let's Encrypt DNS-01) +``` + +**When to use:** When you want to show how TLS works on a Kubernetes-based platform with STACKIT. + +--- + +## Common Architecture Principles + +**TLS termination at the load balancer** +The ALB (or NLB + Traefik IC on Kubernetes) terminates HTTPS. Backend VMs or pods only receive plain HTTP on port 80 over the private network. + +**Infrastructure as Code** +All resources are managed via Terraform. No manual clicking in the portal. + +**Gitignored secrets** +`terraform.tfvars`, `backend.conf`, and `keys/` are gitignored in every showcase. No secret ends up in the repository. + +--- + +## References + +- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) +- [STACKIT Developer Documentation](https://docs.stackit.cloud) +- [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) +- [hashicorp/tls Provider](https://registry.terraform.io/providers/hashicorp/tls/latest/docs) +- [cert-manager](https://cert-manager.io/docs/) diff --git a/examples/alb-tls-examples/alb-k8s/.gitignore b/examples/alb-tls-examples/alb-k8s/.gitignore new file mode 100644 index 0000000..8879cd5 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/.gitignore @@ -0,0 +1,32 @@ +# Terraform +terraform/.terraform/ +terraform/.terraform.lock.hcl +terraform/terraform.tfstate +terraform/terraform.tfstate.backup +terraform/*.tfstate* +terraform/*.tfvars +terraform/backend.conf + +# Kubeconfig (contains cluster credentials) +.kubeconfig +kubeconfig +*.kubeconfig + +# STACKIT Service Account Keys (sensitive) +keys/ +secrets/ +sa.json +*.sa.json + +# Local environment files +.env +.env.local + +# macOS +.DS_Store + +# Editor +.idea/ +.vscode/ +*.swp +*.swo diff --git a/examples/alb-tls-examples/alb-k8s/README.md b/examples/alb-tls-examples/alb-k8s/README.md new file mode 100644 index 0000000..0923b8d --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/README.md @@ -0,0 +1,280 @@ +# SKE TLS Showcase — NLB + Traefik + cert-manager + +Deploys a STACKIT SKE cluster with a Traefik Ingress Controller behind an auto-provisioned NLB. +TLS is handled in-cluster by cert-manager (Let's Encrypt DNS-01 via STACKIT webhook) — no ALB involved. +Serves as a conceptual baseline for the upcoming STACKIT ALB integration on Kubernetes. + +--- + +## Architecture + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant K8s as Kubernetes / SKE + participant CM as cert-manager + participant LE as Let's Encrypt + + Note over User,TF: Phase 1 — terraform apply + + User->>TF: create Project + SKE Cluster + DNS Zone + TF-->>User: cluster_endpoint · zone_id + + Note over User,K8s: Phase 2 — deploy.sh + + User->>K8s: install Traefik Ingress Controller (Helm) + K8s-->>User: nlb_ip (auto-provisioned by SKE) + + User->>TF: create DNS A-Record (domain → nlb_ip) + TF-->>User: zone ready + + User->>K8s: install cert-manager + STACKIT webhook (Helm) + User->>K8s: apply SA Secret + ClusterIssuer + + User->>K8s: deploy nginx app + Ingress + Certificate + + Note over K8s,LE: Phase 3 — cert-manager (automatic) + + CM->>LE: request certificate (DNS-01 challenge) + LE-->>CM: .crt · .pem + + CM->>K8s: store in Secret nginx-tls + K8s-->>User: ✅ https://your-domain ready +``` + +``` +Client → STACKIT DNS → STACKIT NLB (auto) → Traefik Ingress Controller → nginx Pod + └── TLS: cert-manager + Let's Encrypt +``` + +--- + +## Overview + +| Component | Description | +| ---------------------- | ------------------------------------------------------------------------ | +| Resource hierarchy | Project under an existing STACKIT organisation | +| Kubernetes | STACKIT Kubernetes Engine (SKE) cluster | +| Network | NLB auto-provisioned by SKE when Traefik LoadBalancer Service is created | +| Ingress | Traefik Ingress Controller (Helm) | +| Certificate automation | cert-manager + STACKIT DNS-01 webhook (Helm) | +| DNS | STACKIT primary zone + A-record pointing to the NLB IP | +| Application | nginx Deployment + Service + Ingress + Certificate manifest | + +| In this showcase | Not in this showcase | +| ----------------------------------- | -------------------------------------------------- | +| cert-manager + Let's Encrypt DNS-01 | STACKIT ALB Ingress Controller (not yet available) | +| SKE cluster via Terraform | Self-signed or manually managed certs | +| Traefik Ingress Controller | ALB direct Kubernetes integration | +| STACKIT DNS-01 webhook | Multiple namespaces / production Helm values | + +--- + +## Prerequisites + +| Tool | Version | +| ----------- | ------- | +| Terraform | >= 1.3 | +| helm | >= 3.x | +| kubectl | >= 1.28 | +| STACKIT CLI | >= 0.64 | +| jq | any | + +### Required STACKIT permissions + +| Service Account | Permissions | Used by | +| --------------- | -------------------------------------- | --------------------------------- | +| Terraform SA | SKE Admin, DNS Admin, Resource Manager | `terraform apply` | +| DNS SA | DNS Admin (project-scoped) | DNS-01 challenge via cert-manager | + +Place the DNS SA key at `terraform/keys/sa-key.json`. `deploy.sh` creates the Kubernetes Secret from this file automatically. + +### Create service accounts + +```bash +# Terraform SA +stackit iam service-account create \ + --project-id \ + --name "tf-workshop-sa" + +mkdir -p terraform/keys +stackit iam service-account key create \ + --project-id \ + --service-account-email \ + --output-format json > terraform/keys/sa-key.json + +# DNS SA (for cert-manager webhook) +stackit iam service-account create \ + --project-id \ + --name "dns-certmanager-sa" + +stackit iam service-account key create \ + --project-id \ + --service-account-email \ + --output-format json > terraform/keys/sa-key.json +``` + +--- + +## Deployment + +### 1. Configure credentials + +```bash +cd terraform +cp backend.conf.example backend.conf # fill in access_key + secret_key +cp terraform.tfvars.example terraform.tfvars # fill in all values +``` + +### 2. Provision infrastructure + +```bash +terraform init -backend-config=backend.conf +terraform plan +terraform apply +``` + +### 3. Set kubeconfig + +```bash +export KUBECONFIG=$(pwd)/../.kubeconfig +kubectl get nodes +``` + +### 4. Deploy cluster components + +`deploy.sh` sets the DNS A record, installs Helm charts, and applies manifests. +The DNS SA key must exist at `terraform/keys/sa-key.json` (DNS Admin role required). + +```bash +cd .. && bash scripts/deploy.sh +``` + +--- + +## Validation + +```bash +# Monitor certificate issuance +kubectl describe certificate nginx-tls -n nginx-showcase + +# Test HTTPS once the certificate is Ready +curl https://. + +# Verify NLB IP is wired to DNS +dig . +``` + +--- + +## TLS Flow (DNS-01) + +1. cert-manager creates an ACME order with Let's Encrypt +2. Let's Encrypt requests a `_acme-challenge` TXT record +3. STACKIT webhook creates the TXT record via the STACKIT DNS API +4. Let's Encrypt validates and issues the certificate +5. cert-manager stores the certificate in Secret `nginx-tls` +6. Traefik reads the secret and terminates TLS +7. cert-manager auto-renews 30 days before expiry + +--- + +## File Structure + +``` +alb-k8s/ +├── terraform/ +│ ├── 00-backend.tf # S3 backend declaration +│ ├── 00-provider.tf # Provider versions + STACKIT provider config +│ ├── 01-variables.tf +│ ├── 02-resource-hierarchy.tf # Creates STACKIT folder + project +│ ├── 03-network.tf # NLB is auto-provisioned by SKE (no explicit resources) +│ ├── 04-compute.tf # SKE cluster + kubeconfig +│ ├── 05-dns.tf # DNS zone (A record set by deploy.sh) +│ ├── 06-outputs.tf +│ ├── backend.conf # Backend credentials (gitignored) +│ ├── backend.conf.example +│ ├── terraform.tfvars # Active config (gitignored) +│ └── terraform.tfvars.example +├── kubernetes/ +│ ├── cert-manager/ +│ │ ├── 00-stackit-sa-secret.yaml # SA secret template (deploy.sh creates from keys/) +│ │ ├── 01-cluster-issuer.yaml # ClusterIssuer: Let's Encrypt production +│ │ └── 02-certificate.yaml +│ └── nginx/ +│ ├── 00-namespace.yaml +│ ├── 01-deployment.yaml +│ ├── 02-service.yaml +│ └── 03-ingress.yaml +├── docs/ +│ └── architecture.md # Deployment sequence diagram + component overview +├── scripts/ +│ └── deploy.sh +└── terraform/keys/ # SA key JSON files — gitignored +``` + +--- + +## Security + +| File | Git status | Contains | +| ---------------------------- | ---------- | -------------------------- | +| `terraform/terraform.tfvars` | gitignored | Sensitive configuration | +| `terraform/backend.conf` | gitignored | Object Storage access keys | +| `terraform/keys/` | gitignored | Service account JSON keys | + +- SSH access is not directly exposed — cluster access via kubeconfig only +- Never commit SA key JSON files to the repository +- The DNS SA key is the most sensitive credential — it has write access to your DNS zone + +--- + +## Cleanup + +```bash +cd terraform && terraform destroy +``` + +> **Note:** After `terraform destroy`, STACKIT projects remain in "Pending Deletion" for up to 7 days. This is expected STACKIT platform behaviour. + +--- + +## Troubleshooting + +**Certificate stuck in Pending:** + +```bash +kubectl describe certificate nginx-tls -n nginx-showcase +kubectl describe certificaterequest -n nginx-showcase +kubectl logs -n cert-manager deploy/cert-manager +kubectl logs -n cert-manager -l app=stackit-cert-manager-webhook +``` + +Common causes: wrong `projectId` in ClusterIssuer, SA missing DNS permissions, DNS zone in wrong project. + +**Traefik has no external IP:** + +```bash +kubectl get svc -n traefik traefik +``` + +Wait 2–3 minutes — STACKIT provisions the NLB asynchronously. + +**kubeconfig expired:** + +```bash +cd terraform && terraform apply +export KUBECONFIG=$(pwd)/../.kubeconfig +``` + +--- + +## References + +- [Full architecture details](docs/architecture.md) +- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) +- [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) +- [STACKIT Developer Documentation](https://docs.stackit.cloud) +- [cert-manager](https://cert-manager.io/docs/) +- [stackit-cert-manager-webhook](https://github.com/stackitcloud/stackit-cert-manager-webhook) diff --git a/examples/alb-tls-examples/alb-k8s/docs/architecture.md b/examples/alb-tls-examples/alb-k8s/docs/architecture.md new file mode 100644 index 0000000..27d8ed5 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/docs/architecture.md @@ -0,0 +1,144 @@ +# Architecture: alb-k8s + +## Deployment Sequence + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant K8s as Kubernetes / SKE + participant CM as cert-manager + participant LE as Let's Encrypt + + Note over User,TF: Phase 1 — terraform apply + + User->>TF: create Project + SKE Cluster + DNS Zone + TF-->>User: cluster_endpoint · zone_id + + Note over User,K8s: Phase 2 — deploy.sh + + User->>K8s: install Traefik Ingress Controller (Helm) + K8s-->>User: nlb_ip (auto-provisioned by SKE) + + User->>TF: create DNS A-Record (domain → nlb_ip) + TF-->>User: zone ready + + User->>K8s: install cert-manager + STACKIT webhook (Helm) + User->>K8s: apply SA Secret + ClusterIssuer + + User->>K8s: deploy nginx app + Ingress + Certificate + + Note over K8s,LE: Phase 3 — cert-manager (automatic) + + CM->>LE: request certificate (DNS-01 challenge) + LE-->>CM: .crt · .pem + + CM->>K8s: store in Secret nginx-tls + K8s-->>User: ✅ https://your-domain ready +``` + +--- + +# SKE with TLS — NLB + Traefik + cert-manager + +## Traffic Flow + +``` + Client + │ + │ DNS lookup: nginx.alb-k8s-showcase.stackit.gg + ▼ + STACKIT DNS + │ resolves to NLB public IP (set by deploy.sh) + │ + │ HTTPS :443 + ▼ + STACKIT NLB (L4, provisioned automatically by SKE) + │ + │ TCP passthrough + ▼ + Traefik Ingress Controller (L7, in-cluster) + │ TLS termination (cert from Secret: nginx-tls) + │ Host-based routing + HTTP→HTTPS redirect + ▼ + ClusterIP Service: nginx (namespace: nginx-showcase) + │ + ▼ + Pod: nginxinc/nginx-unprivileged:1.27-alpine +``` + +**NLB provisioning:** STACKIT creates the NLB automatically when the Traefik +`Service` of type `LoadBalancer` is applied. The assigned IP is dynamic; `deploy.sh` +reads it and creates the DNS A record via the STACKIT CLI. + +--- + +## TLS Certificate Flow (DNS-01) + +``` + cert-manager stackit-cert-manager-webhook STACKIT DNS API Let's Encrypt + │ │ │ │ + │── new Certificate ────────►│ │ │ + │ │ │ │ + │◄── ACME order ────────────────────────────────────────────────────────│ + │ │ │ │ + │── solve DNS-01 ───────────►│ │ │ + │ │── create TXT record ──►│ │ + │ │ _acme-challenge.nginx │ │ + │ │ .alb-k8s-showcase │ │ + │ │ .stackit.gg │ │ + │ │ │ │ + │◄── challenge ready ──────────────────────────────────────────────────│ + │ │◄── verify TXT ─────────│ │ + │ │ │ │ + │◄── certificate issued ────────────────────────────────────────────────│ + │ │ │ │ + │── delete TXT record ──────►│── delete TXT ─────────►│ │ + │ │ │ │ + │── store in Secret: nginx-tls (namespace: nginx-showcase) +``` + +cert-manager renews automatically 30 days before expiry. + +--- + +## Component Responsibility + +| Component | Provisioned by | Purpose | +| -------------------------- | -------------------------------------- | -------------------------------------------------- | +| STACKIT Folder + Project | Terraform (`02-resource-hierarchy.tf`) | Resource boundary | +| SKE Cluster | Terraform (`04-compute.tf`) | Kubernetes control plane + nodes | +| DNS Zone | Terraform (`05-dns.tf`) | `alb-k8s-showcase.stackit.gg` | +| DNS A Record | `deploy.sh` (stackit CLI) | `nginx.alb-k8s-showcase.stackit.gg → NLB IP` | +| STACKIT NLB | STACKIT (automatic on LB Service) | L4 load balancer | +| Traefik Ingress Controller | Helm (`deploy.sh`) | L7 routing + TLS termination + HTTP→HTTPS redirect | +| cert-manager | Helm (`deploy.sh`) | Certificate lifecycle | +| STACKIT DNS webhook | Helm (`deploy.sh`) | DNS-01 solver | +| SA Secret | `deploy.sh` (kubectl) | Webhook authenticates against STACKIT API | +| nginx Pod | kubectl (`deploy.sh`) | Demo workload | + +--- + +## Namespace Layout + +``` +traefik + └── Deployment: traefik (Traefik IC) + └── Service: traefik (LoadBalancer → NLB) + +cert-manager + └── Deployment: cert-manager + └── Deployment: cert-manager-webhook + └── Deployment: stackit-cert-manager-webhook + └── Secret: stackit-sa-authentication (STACKIT SA key) + └── ClusterIssuer: letsencrypt-prod + +nginx-showcase + └── Deployment: nginx + └── Service: nginx (ClusterIP) + └── Certificate: nginx-tls + └── Secret: nginx-tls (managed by cert-manager) + └── Ingress: nginx +``` + +--- diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml new file mode 100644 index 0000000..9a38fbb --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml @@ -0,0 +1,47 @@ +# 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. + +# Reference template only. Do NOT commit with real credentials. +# deploy.sh creates this secret automatically from terraform/keys/sa-key.json. +# To create manually: kubectl create secret generic stackit-sa-authentication \ +# --namespace cert-manager --from-file=sa.json=/path/to/sa.json +apiVersion: v1 +kind: Secret +metadata: + name: stackit-sa-authentication + namespace: cert-manager + labels: + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: cert-manager +type: Opaque +stringData: + sa.json: | + { + "id": "", + "publicKey": "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----", + "createdAt": "", + "validUntil": "", + "keyType": "USER_MANAGED", + "keyOrigin": "GENERATED", + "keyAlgorithm": "RSA_2048", + "active": true, + "credentials": { + "kid": "", + "iss": "", + "sub": "", + "aud": "", + "privateKey": "-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----" # gitleaks:allow + } + } diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml new file mode 100644 index 0000000..f028e89 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml @@ -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. + +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod + labels: + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: cert-manager +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: + privateKeySecretRef: + name: letsencrypt-prod-account-key + solvers: + - dns01: + webhook: + solverName: stackit + groupName: acme.stackit.de + config: + projectId: + apiBasePath: https://dns.api.stackit.cloud + acmeTxtRecordTTL: 60 diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml new file mode 100644 index 0000000..3b27a70 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml @@ -0,0 +1,35 @@ +# 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. + +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: nginx-tls + namespace: nginx-showcase + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: cert-manager +spec: + secretName: nginx-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + commonName: "nginx.alb-k8s-showcase.stackit.gg" + duration: 2160h0m0s + renewBefore: 720h0m0s + dnsNames: + - "nginx.alb-k8s-showcase.stackit.gg" diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml new file mode 100644 index 0000000..13357ed --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml @@ -0,0 +1,21 @@ +# 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. + +apiVersion: v1 +kind: Namespace +metadata: + name: nginx-showcase + labels: + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml new file mode 100644 index 0000000..15e71a6 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml @@ -0,0 +1,77 @@ +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + namespace: nginx-showcase + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: web +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + template: + metadata: + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/component: web + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 30 + containers: + - name: nginx + image: nginxinc/nginx-unprivileged:1.27-alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + failureThreshold: 3 + resources: + requests: + cpu: "50m" + memory: "32Mi" + limits: + cpu: "100m" + memory: "64Mi" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml new file mode 100644 index 0000000..ec5069f --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml @@ -0,0 +1,35 @@ +# 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. + +apiVersion: v1 +kind: Service +metadata: + name: nginx + namespace: nginx-showcase + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: web +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + ports: + - name: http + protocol: TCP + port: 80 + targetPort: http diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml new file mode 100644 index 0000000..0cf3411 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml @@ -0,0 +1,41 @@ +# 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. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nginx + namespace: nginx-showcase + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl +spec: + ingressClassName: traefik + rules: + - host: "nginx.alb-k8s-showcase.stackit.gg" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nginx + port: + name: http + tls: + - hosts: + - "nginx.alb-k8s-showcase.stackit.gg" + secretName: nginx-tls diff --git a/examples/alb-tls-examples/alb-k8s/scripts/deploy.sh b/examples/alb-tls-examples/alb-k8s/scripts/deploy.sh new file mode 100755 index 0000000..90e999d --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/scripts/deploy.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +KUBECONFIG="$(realpath "${KUBECONFIG:-${REPO_ROOT}/.kubeconfig}")" +export KUBECONFIG + +if [[ ! -f "${KUBECONFIG}" ]]; then + echo "Error: kubeconfig not found at ${KUBECONFIG}" + exit 1 +fi + +echo "Using kubeconfig: ${KUBECONFIG}" + +HELM="helm --kubeconfig ${KUBECONFIG}" +KUBECTL="kubectl --kubeconfig ${KUBECONFIG}" + +TERRAFORM_DIR="${REPO_ROOT}/terraform" + +echo "==> [1/6] Traefik Ingress Controller" +helm repo add traefik https://traefik.github.io/charts +helm repo update traefik +${HELM} upgrade --install traefik traefik/traefik \ + --namespace traefik \ + --create-namespace \ + --set "ports.web.redirectTo.port=websecure" \ + --set service.type=LoadBalancer \ + --wait --timeout 5m + +echo " Waiting for LoadBalancer IP..." +for i in $(seq 1 24); do + LB_IP=$(${KUBECTL} get svc -n traefik traefik \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) + [[ -n "${LB_IP}" ]] && break + sleep 5 +done + +if [[ -z "${LB_IP:-}" ]]; then + echo "Error: LoadBalancer IP not assigned after 2 minutes." + exit 1 +fi +echo " LoadBalancer IP: ${LB_IP}" + +echo "==> [2/6] DNS A record" +PROJECT_ID=$(cd "${TERRAFORM_DIR}" && terraform output -raw project_id) +ZONE_ID=$(cd "${TERRAFORM_DIR}" && terraform output -raw dns_zone_id) +APP_FQDN=$(cd "${TERRAFORM_DIR}" && terraform output -raw app_fqdn) +APP_HOSTNAME="${APP_FQDN%%.*}" + +RS_ID=$(stackit dns record-set list \ + --project-id "${PROJECT_ID}" \ + --zone-id "${ZONE_ID}" \ + -o json 2>/dev/null | \ + jq -r --arg name "${APP_FQDN}." '.[] | select(.name==$name and .type=="A") | .id' 2>/dev/null || true) + +if [[ -n "${RS_ID}" ]]; then + echo " DNS A record already exists, skipping." +else + stackit dns record-set create \ + --project-id "${PROJECT_ID}" \ + --zone-id "${ZONE_ID}" \ + --name "${APP_HOSTNAME}" \ + --type A \ + --record "${LB_IP}" \ + --ttl 300 \ + --async \ + -y + echo " DNS A record created: ${APP_FQDN} → ${LB_IP}" +fi + +echo "==> [3/6] cert-manager" +helm repo add jetstack https://charts.jetstack.io +helm repo update jetstack +if ${HELM} status cert-manager --namespace cert-manager &>/dev/null; then + echo " cert-manager already installed, skipping." +else + ${HELM} install cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true \ + --wait --timeout 5m +fi + +echo "==> [4/6] STACKIT SA secret for cert-manager webhook" +SA_KEY="${REPO_ROOT}/terraform/keys/sa-key.json" +if [[ ! -f "${SA_KEY}" ]]; then + echo "Error: SA key not found at ${SA_KEY}" + exit 1 +fi +${KUBECTL} create secret generic stackit-sa-authentication \ + --namespace cert-manager \ + --from-file=sa.json="${SA_KEY}" \ + --dry-run=client -o yaml | ${KUBECTL} apply -f - + +echo "==> [5/6] STACKIT cert-manager webhook" +${KUBECTL} wait deployment/cert-manager-webhook \ + --namespace cert-manager \ + --for=condition=Available \ + --timeout=120s +helm repo add stackit-cert-manager-webhook \ + https://stackitcloud.github.io/stackit-cert-manager-webhook +helm repo update stackit-cert-manager-webhook +if ${HELM} status stackit-cert-manager-webhook --namespace cert-manager &>/dev/null; then + echo " stackit-cert-manager-webhook already installed, skipping." +else + ${HELM} install stackit-cert-manager-webhook \ + stackit-cert-manager-webhook/stackit-cert-manager-webhook \ + --namespace cert-manager \ + --set stackitSaAuthentication.enabled=true \ + --wait --timeout 5m +fi + +echo "==> [6/6] Kubernetes manifests" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/cert-manager/01-cluster-issuer.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/nginx/00-namespace.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/nginx/01-deployment.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/nginx/02-service.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/cert-manager/02-certificate.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/nginx/03-ingress.yaml" + +echo "" +echo "==> Done. App: https://${APP_FQDN}" +echo " ${KUBECTL} describe certificate nginx-tls -n nginx-showcase" diff --git a/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf b/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf new file mode 100644 index 0000000..dfc2f09 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf @@ -0,0 +1,17 @@ +# 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 { + backend "s3" {} +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/00-provider.tf b/examples/alb-tls-examples/alb-k8s/terraform/00-provider.tf new file mode 100644 index 0000000..0653ec3 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/00-provider.tf @@ -0,0 +1,33 @@ +# 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 { + required_version = ">= 1.3.0" + + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.97.0" + } + local = { + source = "hashicorp/local" + version = ">= 2.4.0" + } + } +} + +provider "stackit" { + default_region = var.region + service_account_key_path = var.service_account_key_path +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/01-variables.tf b/examples/alb-tls-examples/alb-k8s/terraform/01-variables.tf new file mode 100644 index 0000000..7b4303e --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/01-variables.tf @@ -0,0 +1,98 @@ +# 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 "service_account_key_path" { + type = string + description = "Path to the STACKIT Service Account JSON key file (e.g. 'keys/sa-key.json')." +} + +variable "region" { + type = string + description = "STACKIT region." +} + +variable "organization_id" { + type = string + description = "STACKIT Organization ID (parent of the folder)." +} + +variable "folder_name" { + type = string + description = "Name for the STACKIT folder." +} + +variable "owner_email" { + type = string + description = "Project owner email address." +} + +variable "project_name" { + type = string + description = "Name for the new STACKIT project." +} + +variable "cluster_name" { + type = string + description = "SKE cluster name. Maximum 11 characters." +} + +variable "kubernetes_version_min" { + type = string + description = "Minimum Kubernetes version. STACKIT auto-updates within this minor version." +} + +variable "node_machine_type" { + type = string + description = "Worker node machine type (e.g. 'g2i.2' = 2 vCPU / 8 GB RAM)." +} + +variable "node_os_name" { + type = string + description = "Node OS. SKE supports 'flatcar'." +} + +variable "node_min" { + type = string + description = "Minimum node count." +} + +variable "node_max" { + type = string + description = "Maximum node count." +} + +variable "availability_zone" { + type = string + description = "Availability zone for the node pool." +} + +variable "dns_zone_name" { + type = string + description = "STACKIT DNS zone display name." +} + +variable "dns_zone_fqdn" { + type = string + description = "DNS zone FQDN (e.g. 'showcase.example.com'). Must be a domain you control." +} + +variable "dns_contact_email" { + type = string + description = "Contact email for the DNS zone." +} + +variable "app_hostname" { + type = string + description = "Hostname label for the nginx app (e.g. 'nginx' → nginx.showcase.example.com)." +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/02-resource-hierarchy.tf b/examples/alb-tls-examples/alb-k8s/terraform/02-resource-hierarchy.tf new file mode 100644 index 0000000..3e03fd2 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/02-resource-hierarchy.tf @@ -0,0 +1,29 @@ +# 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_resourcemanager_folder" "this" { + name = var.folder_name + parent_container_id = var.organization_id + owner_email = var.owner_email + + lifecycle { + ignore_changes = [labels] + } +} + +resource "stackit_resourcemanager_project" "this" { + name = var.project_name + parent_container_id = stackit_resourcemanager_folder.this.folder_id + owner_email = var.owner_email +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/03-network.tf b/examples/alb-tls-examples/alb-k8s/terraform/03-network.tf new file mode 100644 index 0000000..dee2475 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/03-network.tf @@ -0,0 +1,18 @@ +# 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. + +# No explicit network resources needed. +# STACKIT NLB is provisioned automatically by SKE when the NGINX Ingress +# Controller Service of type LoadBalancer is created. The assigned IP is +# retrieved by deploy.sh and used to create the DNS A record. diff --git a/examples/alb-tls-examples/alb-k8s/terraform/04-compute.tf b/examples/alb-tls-examples/alb-k8s/terraform/04-compute.tf new file mode 100644 index 0000000..db4e0ec --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/04-compute.tf @@ -0,0 +1,57 @@ +# 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_ske_cluster" "this" { + project_id = stackit_resourcemanager_project.this.project_id + name = var.cluster_name + kubernetes_version_min = var.kubernetes_version_min + + node_pools = [ + { + name = "default" + machine_type = var.node_machine_type + os_name = var.node_os_name + minimum = var.node_min + maximum = var.node_max + availability_zones = [var.availability_zone] + volume_type = "storage_premium_perf1" + volume_size = "32" + } + ] + + maintenance = { + enable_kubernetes_version_updates = true + enable_machine_image_version_updates = true + start = "01:00:00Z" + end = "02:00:00Z" + } + + network = { + control_plane = { + access_scope = "PUBLIC" + } + } +} + +resource "stackit_ske_kubeconfig" "this" { + project_id = stackit_resourcemanager_project.this.project_id + cluster_name = stackit_ske_cluster.this.name + refresh = true +} + +resource "local_sensitive_file" "kubeconfig" { + content = stackit_ske_kubeconfig.this.kube_config + filename = "${path.module}/../.kubeconfig" + file_permission = "0600" +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/05-dns.tf b/examples/alb-tls-examples/alb-k8s/terraform/05-dns.tf new file mode 100644 index 0000000..19524ca --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/05-dns.tf @@ -0,0 +1,22 @@ +# 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_dns_zone" "this" { + project_id = stackit_resourcemanager_project.this.project_id + name = var.dns_zone_name + dns_name = var.dns_zone_fqdn + contact_email = var.dns_contact_email + type = "primary" + default_ttl = 300 +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/06-outputs.tf b/examples/alb-tls-examples/alb-k8s/terraform/06-outputs.tf new file mode 100644 index 0000000..ea07993 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/06-outputs.tf @@ -0,0 +1,63 @@ +# 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. + +output "project_id" { + value = stackit_resourcemanager_project.this.project_id + description = "STACKIT Project ID. Required for kubernetes/ manifests and cert-manager ClusterIssuer." +} + +output "cluster_name" { + value = stackit_ske_cluster.this.name + description = "Name of the SKE cluster." +} + +output "dns_zone_id" { + value = stackit_dns_zone.this.zone_id + description = "STACKIT DNS Zone ID." +} + +output "dns_zone_fqdn" { + value = var.dns_zone_fqdn + description = "DNS zone FQDN. The app is reachable at .." +} + +output "app_fqdn" { + value = "${var.app_hostname}.${var.dns_zone_fqdn}" + description = "Fully qualified domain name of the nginx showcase app." +} + +output "kubeconfig_path" { + value = local_sensitive_file.kubeconfig.filename + description = "Path to the generated kubeconfig." +} + +output "next_steps" { + value = <<-EOT + + ✅ Infrastructure deployed. + + 1. Set kubeconfig: + export KUBECONFIG=$(terraform output -raw kubeconfig_path) + + 2. Verify cluster: + kubectl get nodes + + 3. Note project_id for cert-manager config: + terraform output project_id + + 4. Deploy cluster components (sets DNS A record automatically): + cd .. && bash scripts/deploy.sh + EOT + description = "Next steps after terraform apply." +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/07-alb.tf b/examples/alb-tls-examples/alb-k8s/terraform/07-alb.tf new file mode 100644 index 0000000..6aef16c --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/07-alb.tf @@ -0,0 +1,71 @@ +# 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. + +# A native STACKIT ALB Ingress Controller for Kubernetes does not yet exist (mid-2026). +# +# Current traffic path: +# Internet → STACKIT NLB (yawol, auto-provisioned) → Traefik IC → Pod +# +# Target path when available: +# Internet → STACKIT ALB → Kubernetes Service / Ingress → Pod +# +# Expected changes: +# - ingressClassName: traefik → ingressClassName: +# - Traefik IC replaced by ALB as L7 termination point +# - TLS terminates at the ALB; cert-manager integration TBD +# - nginx.ingress.kubernetes.io/* annotations replaced by ALB equivalents +# +# For an external ALB in front of the cluster (available today), see ../alb/. + +# resource "stackit_application_load_balancer" "this" { +# project_id = stackit_resourcemanager_project.this.project_id +# region = var.region +# name = "${var.cluster_name}-alb" +# plan_id = "p10" +# external_address = "" +# +# listeners = [ +# { +# name = "https" +# port = 443 +# protocol = "PROTOCOL_HTTPS" +# https = { +# certificate_config = { +# certificate_ids = [""] +# } +# } +# http = { +# hosts = [{ +# host = var.app_hostname +# rules = [{ target_pool = "ske-nodeport" }] +# }] +# } +# } +# ] +# +# networks = [ +# { +# network_id = "" +# role = "ROLE_LISTENERS_AND_TARGETS" +# } +# ] +# +# target_pools = [ +# { +# name = "ske-nodeport" +# target_port = 30080 +# targets = [] +# } +# ] +# } diff --git a/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example b/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example new file mode 100644 index 0000000..c19f34e --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example @@ -0,0 +1,15 @@ +bucket = "" +key = "" +region = "eu01" + +endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" +} + +access_key = "" +secret_key = "" + +skip_credentials_validation = true +skip_region_validation = true +skip_requesting_account_id = true +skip_s3_checksum = true diff --git a/examples/alb-tls-examples/alb-k8s/terraform/terraform.tfvars.example b/examples/alb-tls-examples/alb-k8s/terraform/terraform.tfvars.example new file mode 100644 index 0000000..a9be74d --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/terraform.tfvars.example @@ -0,0 +1,28 @@ +# cp terraform.tfvars.example terraform.tfvars + +# ── Auth ──────────────────────────────────────────────────────────────────── +service_account_key_path = "keys/sa-key.json" + +# ── STACKIT ───────────────────────────────────────────────────────────────── +region = "eu01" + +# ── Resource Hierarchy ────────────────────────────────────────────────────── +organization_id = "" +folder_name = "alb-k8s-showcase" +owner_email = "firstname.lastname@example.com" +project_name = "alb-k8s-showcase" + +# ── SKE Cluster ───────────────────────────────────────────────────────────── +cluster_name = "alb-k8s" +kubernetes_version_min = "1.33" +node_machine_type = "g2i.2" +node_os_name = "flatcar" +node_min = "1" +node_max = "3" +availability_zone = "eu01-1" + +# ── DNS ───────────────────────────────────────────────────────────────────── +dns_zone_name = "alb-k8s-showcase" +dns_zone_fqdn = "showcase.example.com" +dns_contact_email = "firstname.lastname@example.com" +app_hostname = "nginx" diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.gitignore b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.gitignore new file mode 100644 index 0000000..d807818 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.gitignore @@ -0,0 +1,51 @@ +# ---> Terraform +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +.env +*.qcow2 +.DS_Store +*.bkp +.idea + +keys/* +backend.conf + +# stackit-acme-alb +stackit-acme-alb/.env +stackit-acme-alb/letsencrypt_data/ diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform-version b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform-version new file mode 100644 index 0000000..22708fe --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform-version @@ -0,0 +1 @@ +v1.5.7 diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform.lock.hcl b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform.lock.hcl new file mode 100644 index 0000000..59108ab --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform.lock.hcl @@ -0,0 +1,65 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/random" { + version = "3.6.3" + constraints = "3.6.3" + hashes = [ + "h1:zG9uFP8l9u+yGZZvi5Te7PV62j50azpgwPunq2vTm1E=", + "zh:04ceb65210251339f07cd4611885d242cd4d0c7306e86dda9785396807c00451", + "zh:448f56199f3e99ff75d5c0afacae867ee795e4dfda6cb5f8e3b2a72ec3583dd8", + "zh:4b4c11ccfba7319e901df2dac836b1ae8f12185e37249e8d870ee10bb87a13fe", + "zh:4fa45c44c0de582c2edb8a2e054f55124520c16a39b2dfc0355929063b6395b1", + "zh:588508280501a06259e023b0695f6a18149a3816d259655c424d068982cbdd36", + "zh:737c4d99a87d2a4d1ac0a54a73d2cb62974ccb2edbd234f333abd079a32ebc9e", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a357ab512e5ebc6d1fda1382503109766e21bbfdfaa9ccda43d313c122069b30", + "zh:c51bfb15e7d52cc1a2eaec2a903ac2aff15d162c172b1b4c17675190e8147615", + "zh:e0951ee6fa9df90433728b96381fb867e3db98f66f735e0c3e24f8f16903f0ad", + "zh:e3cdcb4e73740621dabd82ee6a37d6cfce7fee2a03d8074df65086760f5cf556", + "zh:eff58323099f1bd9a0bec7cb04f717e7f1b2774c7d612bf7581797e1622613a0", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.3.0" + constraints = "~> 4.0" + hashes = [ + "h1:5bCU/c+2HUh7GhclzNSH6gAuoCS4inW3obEtRAwu6WQ=", + "zh:0ab58d6f8991d436c7d2dbd89ed814709b949b07ac5a54ee53b0aec1fa772a8b", + "zh:60b347abcb56f45d97c56f14d895069cd15a83993f199777f571b79fea3642ee", + "zh:6889be32640349230de3f23856e6f04e0e9ced4a84a27d3f552fa54684448218", + "zh:73f8e1ecf7135033165fb14b7e8bf4d656f3ce13065ec35762ea0481975328c7", + "zh:94ce25ee253eca0b42cae9c856b36bca8103b6453012d1b279c3623c805f2d42", + "zh:96bc6de9fd67bc446fd11257872e1ffb1029a996ed1d65a3f6b43f6d408ad9ab", + "zh:97c609a310a51bfd504d704e036d72064a84bf0bdb36cc08cd4cc66098212b41", + "zh:a12c16e94533c5bd123f75032576b9dc91dd5d5ccd5f7cf331d0f2e1adc55cf8", + "zh:c4f014f876adf7af57188795050bda5b0029d8c7d7773031102b6c36dcf1fc21", + "zh:d9b0a21583aaa3df3a95394fb949a3c515ff71c2ff5a1fc4a73d364aa90bfca5", + "zh:da510d22f0c6d71ad19a76406f106b782448f512375787ecfabb338ed1e311a7", + "zh:f0e9447a9ce3a24cdaa113089e65663c836d8b9bfdb915a1c0284e0112cab5c0", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/stackitcloud/stackit" { + version = "0.98.0" + constraints = "> 0.95.0" + hashes = [ + "h1:/FB0wBnvmjumjykX+j90kSck6LMScDaYo1STO5Vp/kw=", + "zh:031028340fbaeeb5c4c6b1d5c6d6287a70cf253cfb89f04d462a1c0ab6237ffc", + "zh:0dde99e7b343fa01f8eefc378171fb8621bedb20f59157d6cc8e3d46c738105f", + "zh:0eee18f9a262fa58966c960f1f0863eed92cd953d0f0306ecc456b58cc2911f8", + "zh:1646966ebac0eb5d6c78ac5aa1528921d7a635f14d81300463a402c55e33cfd3", + "zh:5374ab9e5e6d837787b4f18bcf0125a1bf3ee2da40c022cc7695d6879fed111b", + "zh:6a5b9e1307055f8d358373da625ffcb4d77ec44f260d14473b10e5777380765e", + "zh:6c90090504474695ab7290d64386dd988f4fb65c90c74c9cf3a6da6226ae8a70", + "zh:8317218828f29be95ce712863646dc8968e146ec14e5ab258cb1e8f8b649245b", + "zh:9eef08e4fb7a75760f9dc8a422446f19a210ebf8177dd5aeb97444295f0120cf", + "zh:9f2147eee63feae75b96f17f3b3ebab8a29cd7164cdd08eb2bb871e5c425a77f", + "zh:b63ea754eea233292fb73d87a9810104da2bd347abf2ca0da44ac76591dcdddb", + "zh:de60bd928828a836e446f9f89e7a3bfc4e6dd73bac6827914087b34e4ad0c978", + "zh:f22d295b2e4e94ae1566e20fd752825e008a62250cf7243f1161c0bf4e986518", + "zh:f7e57bc7be2cc016983ff3ad50d2733b85e90bfaa7aa9e2192563dc9d422fb07", + ] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-backend.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-backend.tf new file mode 100644 index 0000000..dfc2f09 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-backend.tf @@ -0,0 +1,17 @@ +# 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 { + backend "s3" {} +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-provider.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-provider.tf new file mode 100644 index 0000000..3a04dbd --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-provider.tf @@ -0,0 +1,34 @@ +# 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 { + required_version = ">= 1.5.0" + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.98.0" + } + random = { + source = "hashicorp/random" + version = "3.6.3" + } + } +} + +provider "stackit" { + default_region = var.stackit_region + service_account_key_path = var.stackit_service_account_key_path + # required for stackit_application_load_balancer (beta resource) + enable_beta_resources = true +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/01-variables.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/01-variables.tf new file mode 100644 index 0000000..392343c --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/01-variables.tf @@ -0,0 +1,179 @@ +# 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. + +# ─── Provider ───────────────────────────────────────────────────────────────── + +variable "stackit_region" { + description = "STACKIT region (e.g. eu01)" + type = string + default = "eu01" +} + +variable "stackit_service_account_key_path" { + description = "Relative path to the STACKIT service account key JSON file" + type = string + default = "keys/sa-key.json" +} + +# ─── Resource Hierarchy ─────────────────────────────────────────────────────── + +variable "organization_id" { + description = "STACKIT organisation container ID — parent into which the folder is created (find in the STACKIT Portal under Organisation settings)" + type = string +} + +variable "owner_email" { + description = "Email of the resource owner; used for folder and project creation (must be an existing STACKIT user in the organisation)" + type = string +} + +variable "folder_name" { + description = "Name of the folder to create under the organisation" + type = string + default = "alb-certbot-workshop" +} + +variable "project_name" { + description = "Name of the project to create inside the folder" + type = string + default = "alb-certbot-dev" +} + +# ─── Network ────────────────────────────────────────────────────────────────── + +variable "network_name" { + description = "Name of the private network" + type = string + default = "alb-certbot-net" +} + +variable "network_cidr" { + description = "IPv4 CIDR block for the network (e.g. 10.10.0.0/24)" + type = string + default = "10.10.0.0/24" + + validation { + condition = can(cidrnetmask(var.network_cidr)) + error_message = "network_cidr must be a valid CIDR notation, e.g. 10.10.0.0/24." + } +} + +variable "admin_cidr" { + description = "Source CIDR allowed for SSH (port 22) — use your egress IP, e.g. 203.0.113.10/32. Avoid 0.0.0.0/0." + type = string + + validation { + condition = can(cidrnetmask(var.admin_cidr)) + error_message = "admin_cidr must be a valid CIDR notation, e.g. 203.0.113.10/32." + } +} + +# ─── Compute / VM ───────────────────────────────────────────────────────────── + +variable "vm_name" { + description = "Name of the Docker host VM" + type = string + default = "alb-certbot-docker-host" +} + +variable "machine_type" { + description = "STACKIT machine type — list available: stackit server machine-type list --project-id " + type = string + default = "g1.1" +} + +variable "availability_zone" { + description = "Availability zone for the VM (e.g. eu01-1, eu01-2, eu01-3)" + type = string + default = "eu01-1" +} + +variable "image_id" { + description = "UUID of the boot image (Debian 12 recommended) — list available: stackit image list --all --project-id " + type = string +} + +variable "boot_volume_size_gb" { + description = "Root disk size in GB" + type = number + default = 32 + + validation { + condition = var.boot_volume_size_gb >= 10 + error_message = "boot_volume_size_gb must be at least 10 GB." + } +} + +variable "keypair_name" { + description = "Name of the SSH key pair to register in STACKIT" + type = string + default = "alb-certbot-workshop-key" +} + +variable "ssh_public_key" { + description = "SSH public key string (ssh-ed25519 AAAA... or ssh-rsa AAAA...) — never commit the private key" + type = string + sensitive = true +} + +variable "start_nginx_test_container" { + description = "Start an nginx:alpine test container on port 80 via cloud-init (useful for ALB backend health-check validation)" + type = bool + default = true +} + +# ─── DNS ────────────────────────────────────────────────────────────────────── + +variable "dns_zone_name" { + description = "Human-readable label for the DNS zone resource" + type = string + default = "alb-certbot-workshop-zone" +} + +variable "dns_name" { + description = "DNS zone apex FQDN, e.g. workshop.example.com — must be a domain you control or can delegate" + type = string +} + +variable "dns_contact_email" { + description = "SOA contact email for the DNS zone" + type = string +} + +# ─── ALB (Option A) ─────────────────────────────────────────────────────────── + +variable "alb_name" { + description = "Name of the Application Load Balancer" + type = string + default = "alb-certbot-workshop" +} + +variable "alb_plan_id" { + description = "ALB service plan — p10 is the smallest available plan" + type = string + default = "p10" +} + + +variable "alb_target_pool_name" { + description = "Name of the ALB target pool pointing to the Docker host VM" + type = string + default = "docker-host-pool" +} + +variable "alb_acl_ranges" { + description = "List of source CIDRs allowed to reach the ALB. Use [\"0.0.0.0/0\"] to allow all traffic." + type = list(string) + default = ["0.0.0.0/0"] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/02-resource-hierarchy.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/02-resource-hierarchy.tf new file mode 100644 index 0000000..3aa679b --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/02-resource-hierarchy.tf @@ -0,0 +1,38 @@ +# 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_resourcemanager_folder" "showcase" { + name = var.folder_name + owner_email = var.owner_email + parent_container_id = var.organization_id + + labels = { + environment = "workshop" + managed-by = "terraform" + use-case = "alb-certbot" + } +} + +resource "stackit_resourcemanager_project" "showcase" { + name = var.project_name + owner_email = var.owner_email + parent_container_id = stackit_resourcemanager_folder.showcase.id + + labels = { + environment = "workshop" + managed-by = "terraform" + folder = var.folder_name + use-case = "alb-certbot" + } +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/03-network.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/03-network.tf new file mode 100644 index 0000000..c23f57c --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/03-network.tf @@ -0,0 +1,101 @@ +# 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" "workshop" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = var.network_name + ipv4_prefix = var.network_cidr + ipv4_nameservers = ["8.8.8.8", "1.1.1.1"] + routed = true + + labels = { + environment = "workshop" + managed-by = "terraform" + } +} + +resource "stackit_security_group" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = "${var.vm_name}-sg" + description = "Security group for the ALB/Certbot workshop Docker host" + stateful = true + + labels = { + environment = "workshop" + managed-by = "terraform" + } +} + +resource "stackit_security_group_rule" "ssh_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "SSH from admin CIDR" + + protocol = { name = "tcp" } + port_range = { min = 22, max = 22 } + ip_range = var.admin_cidr +} + +resource "stackit_security_group_rule" "http_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "HTTP for ALB backend and certbot HTTP-01" + + protocol = { name = "tcp" } + port_range = { min = 80, max = 80 } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "https_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "HTTPS ingress" + + protocol = { name = "tcp" } + port_range = { min = 443, max = 443 } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_tcp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow all outbound TCP" + + protocol = { name = "tcp" } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_udp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow all outbound UDP" + + protocol = { name = "udp" } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_icmp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow outbound ICMP" + + protocol = { name = "icmp" } + ip_range = "0.0.0.0/0" +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/04-compute.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/04-compute.tf new file mode 100644 index 0000000..78285b8 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/04-compute.tf @@ -0,0 +1,85 @@ +# 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. + +locals { + cloud_init_rendered = templatefile("${path.root}/templates/cloud-init.yaml.tpl", { + start_nginx_test_container = var.start_nginx_test_container + }) +} + +resource "stackit_key_pair" "workshop" { + name = var.keypair_name + public_key = chomp(var.ssh_public_key) + + labels = { + environment = "workshop" + managed-by = "terraform" + } +} + +resource "stackit_network_interface" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + network_id = stackit_network.workshop.network_id + name = "${var.vm_name}-nic" + security_group_ids = [stackit_security_group.vm.security_group_id] + + # The ALB injects its target_security_group into this NIC after creation — + # ignore_changes prevents Terraform from reverting it on subsequent applies + lifecycle { + ignore_changes = [security_group_ids] + } +} + +resource "stackit_public_ip" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + network_interface_id = stackit_network_interface.vm.network_interface_id + + labels = { + environment = "workshop" + managed-by = "terraform" + } +} + +resource "stackit_server" "docker_host" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = var.vm_name + machine_type = var.machine_type + availability_zone = var.availability_zone + keypair_name = stackit_key_pair.workshop.name + user_data = local.cloud_init_rendered + + boot_volume = { + source_type = "image" + source_id = var.image_id + size = var.boot_volume_size_gb + } + + network_interfaces = [stackit_network_interface.vm.network_interface_id] + + agent = { + provisioning_policy = "ALWAYS" + } + + labels = { + environment = "workshop" + managed-by = "terraform" + role = "docker-host" + use-case = "alb-certbot" + } + + depends_on = [ + stackit_network_interface.vm, + stackit_key_pair.workshop, + ] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/05-dns.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/05-dns.tf new file mode 100644 index 0000000..0eb06cc --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/05-dns.tf @@ -0,0 +1,36 @@ +# 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. + +# Primary DNS zone — used for ACME DNS-01 challenge delegation +# After apply: delegate this zone at your registrar using the output nameservers +resource "stackit_dns_zone" "workshop" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = var.dns_zone_name + dns_name = var.dns_name + contact_email = var.dns_contact_email + type = "primary" + default_ttl = 300 + + description = "Workshop zone for ALB + Certbot ACME DNS-01 challenge" +} + +# A-record pointing the zone apex to the ALB public IP +resource "stackit_dns_record_set" "alb_a" { + project_id = stackit_resourcemanager_project.showcase.project_id + zone_id = stackit_dns_zone.workshop.zone_id + name = var.dns_name + type = "A" + ttl = 300 + records = [stackit_public_ip.alb.ip] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/06-outputs.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/06-outputs.tf new file mode 100644 index 0000000..95ad50f --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/06-outputs.tf @@ -0,0 +1,141 @@ +# 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. + +output "folder_id" { + description = "Container ID of the created folder" + value = stackit_resourcemanager_folder.showcase.id +} + +output "project_id" { + description = "UUID of the created project — use this for all subsequent STACKIT CLI commands" + value = stackit_resourcemanager_project.showcase.project_id +} + +output "network_id" { + description = "UUID of the private network" + value = stackit_network.workshop.network_id +} + +output "vm_public_ip" { + description = "Public IPv4 address of the Docker host" + value = stackit_public_ip.vm.ip +} + +output "dns_zone_id" { + description = "UUID of the DNS zone" + value = stackit_dns_zone.workshop.id +} + +output "dns_primary_nameserver" { + description = "Primary nameserver FQDN for the DNS zone — use for delegation and ACME DNS-01" + value = stackit_dns_zone.workshop.primary_name_server +} + +output "ssh_command" { + description = "SSH command to connect to the Docker host (Debian 12 default user)" + value = "ssh debian@${stackit_public_ip.vm.ip}" +} + +output "nginx_test_url" { + description = "URL to verify the nginx test container" + value = "http://${stackit_public_ip.vm.ip}" +} + +# ─── ALB Outputs ────────────────────────────────────────────────────────────── + +output "alb_public_ip" { + description = "Public IP address of the ALB" + value = stackit_public_ip.alb.ip +} + +output "alb_url" { + description = "HTTP URL of the ALB (DNS must be delegated first)" + value = "http://${var.dns_name}" +} + +output "alb_name" { + description = "ALB name — use to look up the public IP: stackit load-balancer describe --name --project-id " + value = stackit_application_load_balancer.workshop.name +} + +output "alb_private_address" { + description = "ALB private (internal) IP address" + value = stackit_application_load_balancer.workshop.private_address +} + +output "alb_target_security_group" { + description = "Security group ID automatically assigned to ALB targets — already injected into VM NIC" + value = stackit_application_load_balancer.workshop.target_security_group +} + +output "acme_next_steps" { + description = "Phase 2 instructions: replace the bootstrap cert with a Let's Encrypt cert via stackit-acme-alb on the VM" + value = <<-EOT + + ═══════════════════════════════════════════════════════════════ + PHASE 2 — Let's Encrypt certificate via stackit-acme-alb + VM: ${stackit_public_ip.vm.ip} | Domain: ${var.dns_name} + ═══════════════════════════════════════════════════════════════ + + PREREQUISITE: DNS delegation must be active before certbot can run. + Set NS records for ${var.dns_name} to: $(terraform output -raw dns_primary_nameserver) + Verify: dig NS ${var.dns_name} + + ── Step 1: Local — upload files to the VM ──────────────────── + + scp -r stackit-acme-alb debian@${stackit_public_ip.vm.ip}:~/stackit-acme-alb + scp keys/sa-key.json debian@${stackit_public_ip.vm.ip}:~/stackit-acme-alb/sa-key.json + + ── Step 2: Local — encode the SA key as Base64 (macOS) ─────── + + base64 -i keys/sa-key.json | tr -d '\n' + + ── Step 3: SSH to the VM and fill in .env ──────────────────── + + ssh debian@${stackit_public_ip.vm.ip} + cd ~/stackit-acme-alb + + sed -i "s|^PROJECT_ID=.*|PROJECT_ID=${stackit_resourcemanager_project.showcase.project_id}|" .env + sed -i "s|^ALB_NAME=.*|ALB_NAME=${stackit_application_load_balancer.workshop.name}|" .env + sed -i "s|^DOMAIN_WHITELIST=.*|DOMAIN_WHITELIST=${var.dns_name}|" .env + sed -i "s|^DAYS_WARNING=.*|DAYS_WARNING=99999|" .env + sed -i "s|^ALB_SA_KEY_B64=.*|ALB_SA_KEY_B64=$(base64 -w 0 sa-key.json)|" .env + sed -i "s|^DNS_SA_KEY_B64=.*|DNS_SA_KEY_B64=$(base64 -w 0 sa-key.json)|" .env + + grep '^ALB_SA_KEY_B64=' .env | cut -d= -f2- | base64 -d >/dev/null && echo "ALB key OK" + grep '^DNS_SA_KEY_B64=' .env | cut -d= -f2- | base64 -d >/dev/null && echo "DNS key OK" + + # DAYS_WARNING=99999 forces replacement regardless of bootstrap cert expiry. + # Reset to 30 after the first successful run (Step 6). + + ── Step 4: On VM — build the Docker image ──────────────────── + + docker build -t stackit-alb-cert-updater . + + ── Step 5: On VM — run certificate issuance ────────────────── + + docker run --rm \ + --env-file ~/stackit-acme-alb/.env \ + -v ~/stackit-acme-alb/letsencrypt_data:/etc/letsencrypt \ + stackit-alb-cert-updater + + ── Step 6: On VM — cron job for automatic renewal ──────────── + + # Monthly on the 1st at 03:00 (LE certs valid 90 days, warning at 30) + (crontab -l 2>/dev/null; echo "0 3 1 * * docker run --rm --env-file /home/debian/stackit-acme-alb/.env -v /home/debian/stackit-acme-alb/letsencrypt_data:/etc/letsencrypt stackit-alb-cert-updater >> /home/debian/stackit-acme-alb/renewal.log 2>&1") | crontab - + + crontab -l + + EOT +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/07-alb.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/07-alb.tf new file mode 100644 index 0000000..ef14277 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/07-alb.tf @@ -0,0 +1,89 @@ +# 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. + +# Dedicated public IP so Terraform can wire the DNS A-record in a single apply +resource "stackit_public_ip" "alb" { + project_id = stackit_resourcemanager_project.showcase.project_id + + labels = { + environment = "workshop" + managed-by = "terraform" + use-case = "alb" + } +} + +resource "stackit_application_load_balancer" "workshop" { + project_id = stackit_resourcemanager_project.showcase.project_id + region = var.stackit_region + name = var.alb_name + plan_id = var.alb_plan_id + external_address = stackit_public_ip.alb.ip + + networks = [ + { + network_id = stackit_network.workshop.network_id + role = "ROLE_LISTENERS_AND_TARGETS" + } + ] + + listeners = [ + { + name = "http" + port = 80 + protocol = "PROTOCOL_HTTP" + http = { + hosts = [ + { + host = "*" + rules = [{ target_pool = var.alb_target_pool_name }] + } + ] + } + } + ] + + target_pools = [ + { + name = var.alb_target_pool_name + target_port = 80 + targets = [ + { + display_name = "docker-host" + ip = stackit_network_interface.vm.ipv4 + } + ] + } + ] + + options = { + private_network_only = false + access_control = { + allowed_source_ranges = var.alb_acl_ranges + } + } + + labels = { + environment = "workshop" + managed-by = "terraform" + use-case = "alb-certbot" + } + + # certbot patches the HTTPS listener out-of-band via STACKIT API — + # ignore_changes prevents Terraform from reverting those updates + lifecycle { + ignore_changes = [listeners, target_pools] + } + + depends_on = [stackit_network_interface.vm] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md new file mode 100644 index 0000000..1e633d2 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md @@ -0,0 +1,322 @@ +# STACKIT ALB + Let's Encrypt Workshop + +Infrastructure-as-Code workshop environment demonstrating automated TLS certificate lifecycle on STACKIT — Terraform provisions the ALB with an HTTP listener, certbot issues a Let's Encrypt certificate via ACME DNS-01 and attaches it as an HTTPS listener to the ALB. + +--- + +## Overview + +This repository provisions a fully reproducible demo environment on STACKIT using Terraform. It covers the complete path from infrastructure provisioning to automated certificate management. + +| Component | Description | +| ---------------------- | --------------------------------------------------------------------------------------------------------- | +| Resource hierarchy | Folder and project under an existing STACKIT organisation | +| Network | Private routed network (`10.10.0.0/24`) with security groups | +| Compute | Debian 12 VM with Docker Engine (provisioned via cloud-init) | +| DNS | Primary zone for ACME DNS-01 challenge validation | +| Load Balancer | Application Load Balancer with HTTP and HTTPS listeners | +| Certificate automation | [vm-alb-certbot-letsencrypt](stackit-acme-alb/README.md) — PowerShell/certbot container running on the VM | + +--- + +## Architecture + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant VM as certbot VM + participant LE as Let's Encrypt + + Note over User,TF: Phase 1 — terraform apply + + User->>TF: create Network + VM + DNS Zone + TF-->>User: vm_ip · zone ready + + User->>TF: create ALB (HTTP listener + target: vm_ip) + TF-->>User: alb_id · alb_public_ip · http ready + + Note over User,LE: Phase 2 — certbot on VM + + User->>VM: run cert scripts (alb_id) + VM->>LE: request certificate (DNS-01 challenge) + LE-->>VM: .crt · .pem + + VM->>TF: store certificate on STACKIT + VM->>TF: attach HTTPS listener + cert to ALB (alb_id) + TF-->>User: ✅ https://your-domain ready + + Note over VM: Phase 3+ — monthly cron + VM->>LE: renew certificate (auto, < 30 days remaining) +``` + +``` +STACKIT Organisation +└── Folder: alb-certbot-workshop + └── Project: alb-certbot-dev + ├── Network: alb-certbot-net (10.10.0.0/24, routed) + │ └── Security Group: alb-certbot-docker-host-sg + │ ├── Ingress TCP 22 ← admin_cidr only + │ ├── Ingress TCP 80 ← 0.0.0.0/0 (ALB backend traffic) + │ ├── Ingress TCP 443 ← 0.0.0.0/0 + │ └── Egress all → 0.0.0.0/0 + ├── VM: alb-certbot-docker-host (Debian 12) + │ ├── Public IP + │ └── Docker Engine + nginx:alpine (health-check backend) + ├── DNS Zone: alb-workshop.stackit.gg (primary) + └── ALB: alb-certbot-workshop + ├── Public IP (dedicated, wired to DNS A-record by Terraform) + ├── Listener HTTP :80 → VM:80 + └── Listener HTTPS :443 → VM:80 (TLS terminated at ALB) +``` + +The ALB terminates TLS — the backend VM only receives plain HTTP on port 80. This is standard practice: one TLS endpoint, no certificate management on individual backend hosts. + +### Certificate lifecycle + +``` +Phase 1 — terraform apply + ALB provisioned with HTTP listener + target pool (VM private IP). + DNS A-record for the zone apex is created pointing to the ALB public IP. + +Phase 2 — certbot (Docker container, runs on the VM) + Runs certbot DNS-01: writes _acme-challenge TXT record via STACKIT DNS API + Let's Encrypt validates the challenge and issues the certificate + Uploads the certificate to STACKIT Certificate Manager + Patches the ALB — adds HTTPS listener with the new certificate ID via API + +Phase 3+ — monthly cron job on the VM + Checks remaining certificate validity; renews automatically when < 30 days remain +``` + +> `lifecycle.ignore_changes = [listeners]` is set on the ALB resource so that `terraform apply` does not revert certificate updates made by vm-alb-certbot-letsencrypt. + +--- + +## Prerequisites + +### Required tools + +| Tool | Version | Notes | +| -------------------------------------------------------------- | -------- | -------------------------------------------------- | +| [Terraform](https://developer.hashicorp.com/terraform/install) | >= 1.5.0 | Infrastructure provisioning | +| [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) | latest | Image/machine-type lookups, role assignment | +| SSH client | — | VM access | +| Docker | — | Build and run vm-alb-certbot-letsencrypt on the VM | + +### STACKIT service account permissions + +| Scope | Role | Purpose | +| ------------ | -------------------------------- | ---------------------------- | +| Organisation | `resourcemanager.folders.admin` | Create folder | +| Folder | `resourcemanager.projects.admin` | Create project | +| Project | `compute.admin` | VM, network, security groups | +| Project | `dns.admin` | DNS zone and records | +| Project | `load-balancer.admin` | ALB and Certificate Manager | + +> **Note:** Folder and project creation via Terraform requires elevated permissions at the organisation level. As an alternative, create the folder and project manually in the STACKIT Portal and import them into Terraform state. + +### Create and configure the service account + +```bash +# Create service account +stackit iam service-account create \ + --project-id \ + --name "tf-workshop-sa" + +# Generate a key — stored in keys/ (gitignored) +mkdir -p keys +stackit iam service-account key create \ + --project-id \ + --service-account-email \ + --output-format json > keys/sa-key.json + +# Assign load-balancer.admin at project level (required for ALB API) +stackit project member add \ + --project-id \ + --role load-balancer.admin +``` + +--- + +## Deployment + +### 1. Look up required image and machine-type IDs + +```bash +# Debian 12 image UUID +stackit image list --all --project-id + +# Available machine types +stackit server machine-type list --project-id +``` + +### 2. Configure variables + +```bash +cp examples/terraform.tfvars.example terraform.tfvars +``` + +Key values to set: + +```hcl +organization_id = "" +owner_email = "your.name@example.com" +image_id = "" +admin_cidr = "/32" # curl -s https://ifconfig.schwarz +ssh_public_key = "ssh-ed25519 AAAA..." +dns_name = "alb-workshop.stackit.gg" +``` + +### 3. Configure the remote state backend + +```bash +cp examples/backend.conf.example backend.conf +# Fill in: bucket, key, access_key, secret_key (STACKIT Object Storage) +``` + +### 4. Deploy + +```bash +terraform init -backend-config=backend.conf +terraform plan +terraform apply +``` + +--- + +## Validation + +```bash +# Show all outputs — includes pre-filled Phase 2 instructions +terraform output + +# SSH to the VM +ssh debian@$(terraform output -raw vm_public_ip) + +# Verify Docker and the nginx test container +curl http://$(terraform output -raw vm_public_ip) + +# Inspect DNS zone nameservers +terraform output dns_primary_nameserver +``` + +### DNS delegation + +After `terraform apply`, delegate the DNS zone at your registrar by pointing its NS records to the value returned by: + +```bash +terraform output dns_primary_nameserver +``` + +Verify propagation: + +```bash +dig NS alb-workshop.stackit.gg +``` + +--- + +## Phase 2 — Let's Encrypt certificate + +Run `terraform output acme_next_steps` for the complete step-by-step guide with all values pre-filled from your deployment. + +**Quick summary:** + +```bash +# 1. Upload files to the VM +scp -r vm-alb-certbot-letsencrypt debian@:~/vm-alb-certbot-letsencrypt +scp keys/sa-key.json debian@:~/vm-alb-certbot-letsencrypt/sa-key.json + +# 2. SSH to the VM and patch .env with deployment-specific values +ssh debian@ +cd ~/vm-alb-certbot-letsencrypt +# (see: terraform output acme_next_steps → Step 3) + +# 3. Build the image +docker build -t stackit-alb-cert-updater . + +# 4. Staging run first — no Let's Encrypt rate-limit risk +docker run --rm \ + --env-file ~/vm-alb-certbot-letsencrypt/.env \ + -v ~/vm-alb-certbot-letsencrypt/letsencrypt_data:/etc/letsencrypt \ + stackit-alb-cert-updater + +# 5. Switch to production ACME server and run again +# 6. Set up the monthly renewal cron job +# (see: terraform output acme_next_steps → Step 7) +``` + +See [vm-alb-certbot-letsencrypt/readme.md](vm-alb-certbot-letsencrypt/readme.md) for full documentation on the certificate automation container. + +--- + +## File Structure + +``` +. +├── 00-backend.tf # S3-compatible remote state (STACKIT Object Storage) +├── 00-provider.tf # Provider declarations (stackit, random) +├── 01-variables.tf # All input variables with descriptions and defaults +├── 02-resource-hierarchy.tf # Folder and project +├── 03-network.tf # VPC, security group, ingress/egress rules +├── 04-compute.tf # SSH key, NIC, public IP, Debian 12 VM +├── 05-dns.tf # Primary DNS zone + ALB A-record +├── 06-outputs.tf # All outputs including Phase 2 instructions +├── 07-alb.tf # Public IP + Application Load Balancer (HTTP only initially) +│ +├── docs/ +│ └── architecture.md # Deployment sequence diagram + certificate lifecycle +│ +├── templates/ +│ └── cloud-init.yaml.tpl # Docker Engine install + nginx test container +│ +├── examples/ +│ ├── terraform.tfvars.example # Variable template — copy to terraform.tfvars +│ └── backend.conf.example # Backend template — copy to backend.conf +│ +├── vm-alb-certbot-letsencrypt/ # Phase 2: certificate automation (Docker / PowerShell) +│ ├── Dockerfile +│ ├── .env.default # Configuration template +│ ├── .env # Active config — gitignored, never commit +│ └── app/ +│ ├── Main_CertRenew_CLI.ps1 +│ └── lib/ +│ +└── keys/ # Service account key JSON — gitignored +``` + +--- + +## Security + +| File / Directory | Git status | Contains | +| ---------------------------------------------- | ---------- | --------------------------------------- | +| `terraform.tfvars` | gitignored | SSH public key, sensitive configuration | +| `backend.conf` | gitignored | Object Storage access and secret keys | +| `keys/` | gitignored | Service account JSON key | +| `vm-alb-certbot-letsencrypt/.env` | gitignored | Base64-encoded SA key, ACME config | +| `vm-alb-certbot-letsencrypt/letsencrypt_data/` | gitignored | Private keys issued by Let's Encrypt | + +- SSH access is restricted to `admin_cidr` — do not use `0.0.0.0/0` +- Never commit private SSH keys or service account keys to the repository + +--- + +## Cleanup + +```bash +terraform destroy +``` + +> **Note:** After `terraform destroy`, STACKIT projects remain in "Pending Deletion" for up to 7 days. The parent folder cannot be deleted until all child projects are fully removed. This is expected STACKIT platform behaviour. + +--- + +## References + +- [Full architecture details](docs/architecture.md) +- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) +- [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) +- [STACKIT Developer Documentation](https://docs.stackit.cloud) +- [certbot-dns-stackit](https://pypi.org/project/certbot-dns-stackit/) diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/docs/architecture.md b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/docs/architecture.md new file mode 100644 index 0000000..0d72412 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/docs/architecture.md @@ -0,0 +1,135 @@ +# Architecture: vm-alb-certbot-letsencrypt + +## Deployment Sequence + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant VM as certbot VM + participant LE as Let's Encrypt + + Note over User,TF: Phase 1 — terraform apply + + User->>TF: create Network + VM + DNS Zone + TF-->>User: vm_ip · zone ready + + User->>TF: create ALB (HTTP listener + target: vm_ip) + TF-->>User: alb_id · alb_public_ip · http ready + + Note over User,LE: Phase 2 — certbot on VM + + User->>VM: run cert scripts (alb_id) + VM->>LE: request certificate (DNS-01 challenge) + LE-->>VM: .crt · .pem + + VM->>TF: store certificate on STACKIT + VM->>TF: attach HTTPS listener + cert to ALB (alb_id) + TF-->>User: ✅ https://your-domain ready + + Note over VM: Phase 3+ — monthly cron + VM->>LE: renew certificate (auto, < 30 days remaining) +``` + +--- + +# STACKIT ALB + Let's Encrypt (ACME) + +## Traffic Flow + +``` + Client + │ + │ DNS lookup: alb-workshop.stackit.gg + ▼ + STACKIT DNS + │ resolves to ALB public IP (Terraform A-record) + │ + │ HTTPS :443 + ▼ + STACKIT ALB (L7, TLS termination) + │ certificate: Let's Encrypt (managed by vm-alb-certbot-letsencrypt) + │ HTTP routing to target pool + │ + │ HTTP :80 + ▼ + STACKIT VM (Debian 12, Docker Engine) + │ + ▼ + Container: nginx:alpine (port 80, health-check backend) +``` + +The ALB terminates TLS. The VM only receives plain HTTP on port 80 — no +certificate management on the backend. + +--- + +## Three-Phase Deployment + +### Phase 1 — `terraform apply` + +Terraform provisions the complete infrastructure in a single apply: + +| Resource | File | +| ----------------------------------------------------------------- | -------------------------- | +| STACKIT Folder + Project | `02-resource-hierarchy.tf` | +| Network + Security Group | `03-network.tf` | +| VM (Debian 12, Docker Engine) | `04-compute.tf` | +| DNS Zone + A-record → ALB IP | `05-dns.tf` | +| Application Load Balancer (HTTP listener + target: VM private IP) | `07-alb.tf` | + +The ALB is created with an HTTP listener and target pool pointing to the VM. The HTTPS listener is added by certbot in Phase 2. `lifecycle { ignore_changes = [listeners, target_pools] }` prevents Terraform from reverting out-of-band changes. + +### Phase 2 — certbot (Docker container on the VM) + +``` + vm-alb-certbot-letsencrypt container + │ + ├── 1. Run certbot DNS-01 + │ └── write _acme-challenge TXT via STACKIT DNS API + ├── 2. Let's Encrypt validates + issues certificate + ├── 3. Upload certificate to STACKIT Certificate Manager + └── 4. Patch ALB HTTPS listener with new certificate ID +``` + +`lifecycle { ignore_changes = [listeners] }` is set on the ALB resource so +that subsequent `terraform apply` runs do not revert the certificate update. + +### Phase 3 — `terraform apply` + +Wire up target pool (VM private IP) to the ALB so traffic reaches the backend. + +### Phase 4+ — monthly renewal + +A cron job on the VM re-runs the container. It renews automatically when +fewer than 30 days of validity remain. + +--- + +## Certificate Lifecycle (DNS-01) + +``` + vm-alb-certbot-letsencrypt STACKIT DNS API Let's Encrypt STACKIT Cert Mgr ALB + │ │ │ │ │ + │── certbot DNS-01 ─►│ │ │ │ + │ _acme-challenge │ │ │ │ + │ TXT record │ │ │ │ + │ │◄── verify TXT ────│ │ │ + │◄────────────── certificate issued ─────│ │ │ + │── delete TXT ─────►│ │ │ │ + │── upload cert ─────────────────────────────────────────►│ │ + │── PATCH listener ──────────────────────────────────────────────────────►│ +``` + +--- + +## Component Responsibility + +| Component | Provisioned by | Purpose | +| ------------------------- | ---------------------- | ----------------------------------------------- | +| STACKIT Folder + Project | Terraform | Resource boundary | +| Network + Security Group | Terraform | Private network, SSH + HTTP rules | +| VM (Debian 12) | Terraform + cloud-init | Docker host | +| DNS Zone + A-record | Terraform | Zone apex → ALB IP | +| Application Load Balancer | Terraform | HTTP listener initially, HTTPS added by certbot | +| Let's Encrypt certificate | certbot (Docker on VM) | First issuance + monthly renewal | diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/backend.conf.example b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/backend.conf.example new file mode 100644 index 0000000..90ece14 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/backend.conf.example @@ -0,0 +1,15 @@ +bucket = "" +key = "" +region = "eu01" + +endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" +} + +access_key = "" +secret_key = "" + +skip_credentials_validation = true +skip_region_validation = true +skip_requesting_account_id = true +skip_s3_checksum = true diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/terraform.tfvars.example b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/terraform.tfvars.example new file mode 100644 index 0000000..3d159b9 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/terraform.tfvars.example @@ -0,0 +1,51 @@ +# Copy this file to terraform.tfvars and fill in your values. +# terraform.tfvars is gitignored — never commit real values. + +# ─── Provider ───────────────────────────────────────────────────────────────── + +stackit_region = "eu01" +stackit_service_account_key_path = "keys/sa-key.json" + +# ─── Resource Hierarchy ─────────────────────────────────────────────────────── + +# Find your Organisation ID in the STACKIT Portal → Organisation → Settings +organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +owner_email = "your.name@example.com" +folder_name = "alb-certbot-workshop" +project_name = "alb-certbot-dev" + +# ─── Network ────────────────────────────────────────────────────────────────── + +network_name = "alb-certbot-net" +network_cidr = "10.10.0.0/24" + +# Restrict SSH to your IP — never use 0.0.0.0/0 in production +# Find your IP: curl -s https://ifconfig.schwarz +admin_cidr = "203.0.113.10/32" + +# ─── Compute / VM ───────────────────────────────────────────────────────────── + +vm_name = "alb-certbot-docker-host" +availability_zone = "eu01-1" + +# List available machine types: +# stackit server machine-type list --project-id +machine_type = "g1.1" + +# List available images (Debian 12 recommended): +# stackit image list --all --project-id +image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +boot_volume_size_gb = 32 + +keypair_name = "alb-certbot-workshop-key" +ssh_public_key = "ssh-ed25519 AAAA... your-key-comment" + +start_nginx_test_container = true + +# ─── DNS ────────────────────────────────────────────────────────────────────── + +dns_zone_name = "alb-certbot-workshop-zone" +dns_name = "workshop.example.com" +dns_contact_email = "your.name@example.com" diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.env.default b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.env.default new file mode 100644 index 0000000..949e21b --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.env.default @@ -0,0 +1,20 @@ +PROJECT_ID= +REGION_ID=eu01 +ALB_NAME= +DOMAIN_WHITELIST= +DAYS_WARNING=30 + +# Path where Certbot saves the certificates (Default: /etc/letsencrypt/live) +CERTBOT_LIVE_PATH=/etc/letsencrypt/live + +# Mode Switch: set to 'true' for Delegation Mode (Mode 2), 'false' for Direct Mode (Mode 1) +USE_CHALLENGE_DELEGATION=true +VERIFY_ZONE_FQDN= + +# Service Account Keys (Base64 encoded) +ALB_SA_KEY_B64= +DNS_SA_KEY_B64= + +# Default: Let's Encrypt staging (safe for testing — no rate limits). +# Switch to production before deploying for real: https://acme-v02.api.letsencrypt.org/directory +ACME_SERVER=https://acme-staging-v02.api.letsencrypt.org/directory diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.gitignore b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.gitignore new file mode 100644 index 0000000..3f4e08a --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.gitignore @@ -0,0 +1,64 @@ +### macOS +# Finder metadata +.DS_Store + +# Thumbnails +._* + +# Custom folder icons +Icon + +# Volume root files +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +### Windows +# Windows thumbnail cache files +Thumbs.db + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows shortcuts +*.lnk + +### Linux +# Backup files +*~ + +# Temporary files from deleted open files +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder +.Trash-* + +# NFS temporary files +.nfs* + +### VS Code +# VSCode settings (keep shared configuration) +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Custom +.env diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/Dockerfile b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/Dockerfile new file mode 100644 index 0000000..7d246a0 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/Dockerfile @@ -0,0 +1,71 @@ +# ----------------------------------------------------------------------------- +# STACKIT ALB Certificate Auto-Updater +# Description: Containerized environment to automate Let's Encrypt DNS-01 +# challenges and update STACKIT Application Load Balancers. +# +# DISCLAIMER: This implementation is provided as a baseline/Proof of Concept. +# Depending on your organization's specific requirements, security +# policies, and special use cases, you may need to adapt and +# extend the underlying scripts (e.g., adding custom alerting, +# advanced error handling, or specific secret management integrations). +# ----------------------------------------------------------------------------- + +FROM mcr.microsoft.com/powershell:7.4-debian-12 + +# Standard OCI Metadata Labels +LABEL org.opencontainers.image.title="STACKIT ALB Cert Updater" +LABEL org.opencontainers.image.description="Automates ACME certificate renewal for STACKIT Application Load Balancers" +LABEL org.opencontainers.image.authors="Schwarz Digits Cloud GmbH & Co. KG" + +# Disable interactive prompts during apt package installation +ENV DEBIAN_FRONTEND=noninteractive + + +# ========================================== +# PHASE 1: SYSTEM BASE & SECURITY +# ========================================== +# Install core utilities and update existing packages to fix CVEs +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + curl \ + gnupg \ + python3 \ + python3-venv && \ + rm -rf /var/lib/apt/lists/* + + +# ========================================== +# PHASE 2: STACKIT CLI +# ========================================== +# Add official STACKIT repository and install the CLI +RUN curl -sL https://packages.stackit.cloud/keys/key.gpg | gpg --dearmor -o /usr/share/keyrings/stackit.gpg && \ + echo "deb [signed-by=/usr/share/keyrings/stackit.gpg] https://packages.stackit.cloud/apt/cli stackit main" > /etc/apt/sources.list.d/stackit.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends stackit && \ + rm -rf /var/lib/apt/lists/* + + +# ========================================== +# PHASE 3: CERTBOT & ACME DNS PLUGIN +# ========================================== +# Set up a PEP-668 compliant Virtual Environment +ENV VIRTUAL_ENV=/opt/certbot-venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN python3 -m venv $VIRTUAL_ENV + +# Upgrade the venv's pip and install Certbot with the STACKIT plugin +RUN pip3 install --no-cache-dir --upgrade pip setuptools && \ + pip3 install --no-cache-dir certbot certbot-dns-stackit + + +# ========================================== +# PHASE 4: APPLICATION WORKSPACE +# ========================================== +WORKDIR /app + +# Copy the application scripts into the container +COPY app/ /app/ + +# Define the default execution command +ENTRYPOINT ["pwsh", "-File", "/app/Main_CertRenew_CLI.ps1"] diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/README.md b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/README.md new file mode 100644 index 0000000..7ddc15c --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/README.md @@ -0,0 +1,201 @@ +# STACKIT ALB Certificate Auto-Updater + +> **DISCLAIMER:** This repository provides a Proof of Concept (PoC) and baseline implementation. It was successfully developed and tested on a Windows 10 environment using Docker Desktop. Depending on your organization's specific requirements, security policies, and special use cases, you must adapt and extend the underlying scripts (e.g., adding custom alerting, advanced error handling, or specific secret management integrations) before deploying this to a production environment. + +## Overview + +Securing web traffic via TLS/SSL is a standard requirement for applications exposed through a STACKIT Application Load Balancer (ALB). When using Let's Encrypt certificates, an automated renewal process is highly recommended due to their short 90-day validity period. + +This solution automates the provisioning, uploading, and updating of ACME certificates without requiring inbound HTTP traffic to a validation endpoint. It leverages the **DNS-01 challenge** via the STACKIT DNS API, uploads the resulting certificates to the STACKIT Certificate Manager, and patches the Application Load Balancer to use the new certificates seamlessly. + +--- + +## How it Works + +This automation supports two different workflows for validating domain ownership via DNS, depending on where the target domain's DNS zone is hosted. + +### Mode 1: Direct DNS Update (Normal Challenge) + +**Use Case:** The target domain (e.g., `web.yourdomain.com`) is hosted directly within a STACKIT DNS Zone that the provided Service Account has access to. + +In this mode, the script uses the official STACKIT Certbot DNS plugin to create the `_acme-challenge` TXT record directly in the domain's primary DNS zone. + +**High-Level Architecture:**
+![Simple Normal DNS Challenge](./docs/images/mode1_high_level.png) + + + +**Detailed Technical Sequence:**
+![Detailed Normal DNS Challenge](./docs/images/mode1_detail_level.png) + +## + +### Mode 2: ACME Challenge Delegation via CNAME + +**Use Case:** Managing Let's Encrypt renewals for external domains (e.g., domains owned by end-customers). You do not have (and should not require) API credentials to directly modify their production DNS zones. + +To solve this, this automation leverages **ACME Challenge Delegation via CNAME**. This allows Let's Encrypt to validate domain ownership by redirecting the challenge request to a central, isolated "Verification Zone" hosted on STACKIT. The customer only needs to create a one-time CNAME record pointing to this central zone. + +**High-Level Architecture:**
+![Simple Delegated DNS Challenge](./docs/images/mode2_high_level.png) + + + +**Detailed Technical Sequence:**
+![Detailed Delegated DNS Challenge](./docs/images/mode2_detail_level.png) + +## + +## Prerequisites + +To run this automation, you need the following STACKIT resources and credentials: + +1. **A STACKIT Project** with an active **Application Load Balancer** (ALB). +2. **A Target DNS Zone OR a Verification DNS Zone:** Depending on the mode you choose. +3. **DNS Validator Service Account:** A STACKIT Service Account strictly scoped to manage TXT records in the relevant DNS Zone. +4. **ALB Manager Service Account:** A STACKIT Service Account with permissions for the STACKIT Certificate Manager (`POST`/`GET`/`DELETE`) and the Application Load Balancer API (`GET`/`PUT`). + +_Note: You can pass these credentials either as local JSON files or as Base64 encoded environment variables (recommended for Docker/CI)._ + +## Repository Structure + +```text +. +├── Dockerfile # Debian-based image definition with Certbot & STACKIT CLI +├── README.md # This documentation +├── keys/ # (Local execution only) Directory for Service Account JSON keys +└── app/ + ├── Main_CertRenew_CLI.ps1 # The main orchestration script + └── lib/ + ├── Get-StackitAlbCertStatus_CLI.ps1 # Helper: Evaluates ALB cert expiration + ├── StackitHelper_CLI.ps1 # Helper: Handles API uploads & ALB patching + ├── Stackit-DnsHook.ps1 # Hook: Creates TXT records in Verification Zone (Mode 2) + └── Stackit-CleanupHook.ps1 # Hook: Deletes TXT records after validation (Mode 2) +``` + +## Execution Methods + +The script is highly flexible. You can run it via Docker (using Base64 environment variables) or locally (using JSON files). + +### Option A: Running via Docker (Recommended for CI/CD) + +For Docker and CI/CD pipelines, passing JSON files via volume mounts is cumbersome and a security risk. Instead, this automation supports passing the Service Account credentials directly as **Base64 encoded strings** via environment variables. The script will safely decode them in memory during execution. + +**1. Convert your `.json` Service Account keys to Base64 (single line):** + +- _Linux/macOS:_ `base64 -w 0 alb-manager-sa.json` +- _PowerShell:_ `[convert]::ToBase64String([IO.File]::ReadAllBytes("alb-manager-sa.json"))` + +**2. Create an `.env` file** in the root of the repository: + +```env +PROJECT_ID=1b1a69ac-1482-4b44-9165-3470c0b8fb79 +REGION_ID=eu01 +ALB_NAME=your-alb-name +DOMAIN_WHITELIST=web.yourdomain.com,api.yourdomain.com +DAYS_WARNING=30 + +# Path where Certbot saves the certificates (Default: /etc/letsencrypt/live) +CERTBOT_LIVE_PATH=/etc/letsencrypt/live + +# Mode Switch: set to 'true' for Delegation Mode (Mode 2), 'false' for Direct Mode (Mode 1) +USE_CHALLENGE_DELEGATION=true +VERIFY_ZONE_FQDN=alb-verify-poc-ldb-acme.stackit.zone + +# Service Account Keys (Base64 encoded) +ALB_SA_KEY_B64= # gitleaks:allow +DNS_SA_KEY_B64= # gitleaks:allow + +# Use Staging for testing to avoid Let's Encrypt rate limits! +ACME_SERVER=https://acme-staging-v02.api.letsencrypt.org/directory +# For Production, comment out the line above and use: +# ACME_SERVER=https://acme-v02.api.letsencrypt.org/directory +``` + +**3. Build and Run the Docker Container:** +You only need to mount the volume for Certbot to persist the certificates. You do **not** need to mount the `keys` folder! + +Linux: + +```bash +docker build -t stackit-alb-cert-updater . + +docker run --rm -it \ + --env-file .env \ + -v "$(pwd)/letsencrypt_data:/etc/letsencrypt" \ + stackit-alb-cert-updater +``` + +Windows: + +```powershell +docker build -t stackit-alb-cert-updater . + +docker run --rm -it ` + --env-file .env ` + -v "${PWD}\letsencrypt_data:/etc/letsencrypt" ` + stackit-alb-cert-updater +``` + +--- + +### Option B: Running Locally (PowerShell) + +If you are running the script locally (e.g., for testing on your workstation), you can simply pass the direct file paths to your `.json` Service Account keys. + +Linux: + +```powershell +./app/Main_CertRenew_CLI.ps1 ` + -ProjectId "1b1a69ac-1482-4b44-9165-3470c0b8fb79" ` + -RegionId "eu01" ` + -AlbName "your-alb-name" ` + -DomainWhitelist "www.customerapplication.domain", "www2.customerapplication.domain" ` + -UseChallengeDelegation $true ` + -VerifyZoneFQDN "alb-verify-example.stackit.zone" ` + -CertbotLive "/etc/letsencrypt/live" ` + -DNS_SAKeyPath "./keys/dns-validator-sa.json" ` + -ALB_SAKeyPath "./keys/alb-manager-sa.json" ` + -AcmeServer "https://acme-staging-v02.api.letsencrypt.org/directory" +``` + +Windows: + +```powershell +.\app\Main_CertRenew_CLI.ps1 ` + -ProjectId "1b1a69ac-1482-4b44-9165-3470c0b8fb79" ` + -RegionId "eu01" ` + -AlbName "your-alb-name" ` + -DomainWhitelist "www.customerapplication.domain", "www2.customerapplication.domain" ` + -UseChallengeDelegation $true ` + -VerifyZoneFQDN "alb-verify-example.stackit.zone" ` + -CertbotLive "C:\Certbot\live" ` + -DNS_SAKeyPath "./keys/dns-validator-sa.json" ` + -ALB_SAKeyPath "./keys/alb-manager-sa.json" ` + -AcmeServer "https://acme-staging-v02.api.letsencrypt.org/directory" +``` + +## 💡 Important Note on Let's Encrypt Caching (For Testing) + +When testing the renewal process, please be aware of Let's Encrypt's **Authorization Caching**. Once a domain has been successfully validated via the DNS-01 challenge, Let's Encrypt caches this successful validation for **30 days**. +If you trigger a forced renewal for the exact same domain within this window, Let's Encrypt will skip the DNS challenge entirely. Consequently, Certbot will **not** execute the DNS plugin or hook scripts (creation/deletion of the TXT record). To forcefully test the entire workflow including the DNS hooks, you must either test with a new, unvalidated subdomain or ensure you clear your local Certbot account data while using the Staging Environment. + +## Limitations & Out of Scope (PoC Boundaries) + +As this is a baseline Proof of Concept intended for integration into existing CI/CD environments, the following features are deliberately **not implemented** and should be managed by the surrounding automation platform: + +- **CI/CD Pipeline Configurations:** This repository provides the core automation logic via Docker and PowerShell but does _not_ include ready-to-use pipeline definitions (e.g., `.gitlab-ci.yml`, GitHub Actions workflows, or Kubernetes `CronJob` manifests). The consumer must wrap this container into their own scheduling execution engine. +- **Enterprise Secret Management:** While the script supports Base64 encoded environment variables for secure pipeline injection, native API integration with external secret management solutions (e.g., HashiCorp Vault, Azure Key Vault, AWS Secrets Manager) is out of scope. +- **Orphaned Certificate Cleanup:** The script uploads new certificates but does _not_ auto-delete expired or detached certificates from the STACKIT Certificate Manager. This design choice prevents the accidental deletion of certificates that might still be bound to other resources. Housekeeping of orphaned certificates remains a separate operational process. +- **Multi-Domain (SAN) & Wildcard Certificates:** This workflow is heavily optimized for a 1:1 certificate-to-domain mapping. Grouping multiple domains under a single Subject Alternative Name (SAN) certificate or handling `*.domain.com` wildcards requires manual extensions to the underlying Certbot arguments and ALB patching logic. +- **Advanced Alerting & Notifications:** The script outputs clear status and error messages to standard `stdout`/`stderr` streams. It does not natively implement webhooks for chat platforms (e.g., MS Teams, Slack) or Email. It relies on the overarching CI/CD platform to scrape logs and trigger alerts based on the exit code (0 for success, >0 for errors). +- **Automated Rollbacks:** If the patching of the Application Load Balancer fails _after_ a new certificate has been successfully uploaded to STACKIT, the script will throw an error but will not automatically remove the newly uploaded certificate. +- **Concurrency & State Locking:** There is no native distributed state-locking mechanism. Running multiple instances of this script simultaneously against the same ALB configuration could lead to race conditions during the patching phase. + +## Next Steps / Automation + +For continuous operation, you should schedule this script/container to run regularly (e.g., daily). You can achieve this via: + +- A standard Linux `cron` job or in windows (using the task scheduler) on a dedicated server. +- A CI/CD Pipeline schedule (e.g., GitLab CI, GitHub Actions). +- A Kubernetes `CronJob` within STACKIT Kubernetes Engine (SKE), securely mounting the service account keys as Kubernetes Secrets. diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/Main_CertRenew_CLI.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/Main_CertRenew_CLI.ps1 new file mode 100644 index 0000000..fe0f95d --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/Main_CertRenew_CLI.ps1 @@ -0,0 +1,263 @@ +<# +.SYNOPSIS + STACKIT ALB Certificate Auto-Renewal Orchestrator (PoC) + +.DESCRIPTION + This script automates the renewal of Let's Encrypt certificates for a STACKIT Application Load Balancer. + It identifies expiring certificates, triggers Certbot to perform a DNS-01 challenge, uploads the + renewed certificates to the STACKIT Certificate Manager, and patches the ALB configuration. + + This is intended as a Proof of Concept (PoC) baseline for CI/CD integration. + It supports passing Service Account keys as Base64 environment variables to avoid persisting secrets on disk. +#> + +param ( + # Core Infrastructure + [string] $ProjectId = $env:PROJECT_ID, + [string] $RegionId = $(if ($env:REGION_ID) { $env:REGION_ID } else { "eu01" }), + [string] $AlbName = $env:ALB_NAME, + + # Optional filtering: Only renew domains listed here + [string[]] $DomainWhitelist = @(), + + # Paths & Credentials + [string] $CertbotLive = $(if ($env:CERTBOT_LIVE_PATH) { $env:CERTBOT_LIVE_PATH } else { "/etc/letsencrypt/live" }), + [string] $DNS_SAKeyPath = $(if ($env:DNS_SA_KEY_PATH) { $env:DNS_SA_KEY_PATH } else { "$PSScriptRoot/keys/dns-validator-sa.json" }), + [string] $ALB_SAKeyPath = $(if ($env:ALB_SA_KEY_PATH) { $env:ALB_SA_KEY_PATH } else { "$PSScriptRoot/keys/alb-manager-sa.json" }), + + # Execution Modifiers + [bool] $SkipCertbot = $(if ($env:SKIP_CERTBOT -match '^(true|1|yes)$') { $true } else { $false }), + [int] $DaysWarning = $(if ($env:DAYS_WARNING) { [int]$env:DAYS_WARNING } else { 30 }), + + # API Endpoints + [string] $AlbBaseUrl = $(if ($env:ALB_BASE_URL) { $env:ALB_BASE_URL } else { "https://alb.api.stackit.cloud/v2" }), + [string] $CertBaseUrl = $(if ($env:CERT_BASE_URL) { $env:CERT_BASE_URL } else { "https://certificates.api.stackit.cloud/v2" }), + [string] $AcmeServer = $(if ($env:ACME_SERVER) { $env:ACME_SERVER } else { "https://acme-staging-v02.api.letsencrypt.org/directory" }), + + # Advanced: CNAME Delegation Mode configuration + [bool] $UseChallengeDelegation = $(if ($env:USE_CHALLENGE_DELEGATION -match '^(true|1|yes)$') { $true } else { $false }), + [string] $VerifyZoneFQDN = $env:VERIFY_ZONE_FQDN +) + +$ErrorActionPreference = "Stop" +Write-Output "=== STACKIT ALB Certificate Auto-Renewal Pipeline ===" + +# Parse whitelist from environment if not provided via parameter +if ($DomainWhitelist.Count -eq 0 -and $env:DOMAIN_WHITELIST) { + $DomainWhitelist = $env:DOMAIN_WHITELIST -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" } +} + +$Config = @{ + ProjectId = $ProjectId + RegionId = $RegionId + AlbName = $AlbName + DomainWhitelist = $DomainWhitelist + CertbotLive = $CertbotLive + DNS_SAKeyPath = $DNS_SAKeyPath + ALB_SAKeyPath = $ALB_SAKeyPath + SkipCertbot = $SkipCertbot + DaysWarning = $DaysWarning + AlbBaseUrl = $AlbBaseUrl + CertBaseUrl = $CertBaseUrl + AcmeServer = $AcmeServer + UseChallengeDelegation = $UseChallengeDelegation + VerifyZoneFQDN = $VerifyZoneFQDN +} + +# --- PRE-FLIGHT CHECKS --- +Write-Output "`n>>> [INIT] Starting pre-flight checks..." + +# CI/CD Integration: Decode Base64 encoded Service Account keys injected via Environment Variables. +# This avoids storing physical JSON files in the repository or mounting them insecurely. +if ($env:ALB_SA_KEY_B64) { + Write-Output " [INFO] Decoding ALB Service Account from Base64 variable..." + $albBytes = [System.Convert]::FromBase64String($env:ALB_SA_KEY_B64.Trim()) + $Config.ALB_SAKeyPath = Join-Path ([System.IO.Path]::GetTempPath()) "alb-sa.json" + [System.IO.File]::WriteAllBytes($Config.ALB_SAKeyPath, $albBytes) +} + +if ($env:DNS_SA_KEY_B64) { + Write-Output " [INFO] Decoding DNS Service Account from Base64 variable..." + $dnsBytes = [System.Convert]::FromBase64String($env:DNS_SA_KEY_B64.Trim()) + $Config.DNS_SAKeyPath = Join-Path ([System.IO.Path]::GetTempPath()) "dns-sa.json" + [System.IO.File]::WriteAllBytes($Config.DNS_SAKeyPath, $dnsBytes) +} + +# Verify that key files exist (either provided directly or decoded above) +if (-not (Test-Path $Config.DNS_SAKeyPath)) { + Write-Error "[FATAL] DNS Service Account Key missing at $($Config.DNS_SAKeyPath)" + exit 1 +} +if (-not (Test-Path $Config.ALB_SAKeyPath)) { + Write-Error "[FATAL] ALB Service Account Key missing at $($Config.ALB_SAKeyPath)" + exit 1 +} + +if ($Config.UseChallengeDelegation -and [string]::IsNullOrWhiteSpace($Config.VerifyZoneFQDN)) { + Write-Error "[FATAL] Challenge Delegation is enabled, but VERIFY_ZONE_FQDN is empty!" + exit 1 +} + +# Import helper functions +. "$PSScriptRoot/lib/Get-StackitAlbCertStatus_CLI.ps1" +. "$PSScriptRoot/lib/StackitHelper_CLI.ps1" +Write-Output "[OK] Pre-flight checks passed." + +# --- STEP 1: AUTHENTICATION --- +Write-Output "`n>>> [STEP 1] Activating STACKIT CLI Service Account (ALB Scope)..." +& stackit auth activate-service-account --service-account-key-path $Config.ALB_SAKeyPath +if ($LASTEXITCODE -ne 0) { throw "Failed to activate STACKIT CLI Service Account for ALB operations." } +Write-Output "[OK] STACKIT CLI successfully authenticated." + +# --- STEP 2: STATUS CHECK --- +# Query the ALB configuration to map domains to active certificates and calculate expiration +Write-Output "`n>>> [STEP 2] Fetching ALB configuration and certificate status..." +$status = Get-StackitAlbCertStatus -ProjectId $Config.ProjectId -RegionId $Config.RegionId -AlbName $Config.AlbName -Whitelist $Config.DomainWhitelist -DaysWarning $Config.DaysWarning -AlbBaseUrl $Config.AlbBaseUrl -CertBaseUrl $Config.CertBaseUrl + +$allCerts = @($status.certificates) +$toRenew = @($allCerts | Where-Object { $_.shouldReplace -eq $true }) +$healthyCerts = @($allCerts | Where-Object { $_.shouldReplace -eq $false }) + +Write-Output "[INFO] Evaluated a total of $($allCerts.Count) active certificate(s) on ALB '$($Config.AlbName)'." + +# Sub-Check 2a: Warn if a whitelisted domain is missing from the ALB configuration +if ($Config.DomainWhitelist.Count -gt 0) { + $albDomains = $allCerts.domain + foreach ($wlDomain in $Config.DomainWhitelist) { + if ($albDomains -notcontains $wlDomain) { + Write-Warning " [SKIP] Domain '$wlDomain' is on the whitelist, but is NOT configured on the ALB. Skipping..." + } + } +} + +# Sub-Check 2b: Log healthy certificates that do not require renewal yet +if ($healthyCerts.Count -gt 0) { + foreach ($hc in $healthyCerts) { + Write-Output " [SKIP] Certificate for '$($hc.domain)' is healthy (Expires in $($hc.daysUntilExpiry) days). No action required." + } +} + +if ($toRenew.Count -eq 0) { + Write-Output "`n[RESULT] All targeted certificates are healthy and up to date. No renewals required." + Write-Output "=== Workflow Completed SUCCESSFULLY ===" + exit 0 +} + +# --- STEP 3 & 4: RENEWAL LOOP --- +Write-Output "`n>>> [STEP 3] Starting renewal process for $($toRenew.Count) certificate(s)..." + +$WorkflowHasErrors = $false + +foreach ($cert in $toRenew) { + Write-Output "`n--------------------------------------------------------" + Write-Output "[*] Target Domain : $($cert.domain)" + Write-Output "[*] Expiring Cert : $($cert.certificateId)" + Write-Output "--------------------------------------------------------" + + $certbotSuccess = $false + $domainPath = Join-Path $Config.CertbotLive $cert.domain + $fullchainPath = Join-Path $domainPath "fullchain.pem" + $privkeyPath = Join-Path $domainPath "privkey.pem" + + if (-not $Config.SkipCertbot) { + Write-Output " -> [Action] Triggering Certbot ACME DNS-01 challenge..." + + # --- CERTBOT EXTERNAL LOG BOUNDARY --- + Write-Output "" + Write-Output " v===================== CERTBOT OUTPUT =====================v" + + if ($Config.UseChallengeDelegation) { + Write-Output " -> [INFO] Mode: CNAME Delegation Hooks (Verify Zone: $($Config.VerifyZoneFQDN))" + + # Export variables required by the child processes (the PowerShell Hook scripts) + $env:STACKIT_PROJECT_ID = $Config.ProjectId + $env:DNS_SA_KEY_PATH = $Config.DNS_SAKeyPath + $env:VERIFY_ZONE_FQDN = $Config.VerifyZoneFQDN + + $PSExe = if ($PSVersionTable.PSVersion.Major -ge 6) { "pwsh" } else { "powershell" } + + $AuthHookPath = Join-Path $PSScriptRoot "lib\Stackit-DnsHook.ps1" + $CleanupHookPath = Join-Path $PSScriptRoot "lib\Stackit-CleanupHook.ps1" + + $AuthCmd = "$PSExe -NoProfile -ExecutionPolicy Bypass -File `"$AuthHookPath`"" + $CleanupCmd = "$PSExe -NoProfile -ExecutionPolicy Bypass -File `"$CleanupHookPath`"" + + $CertbotArgs = @( + "certonly", + "--manual", + "--manual-auth-hook", $AuthCmd, + "--manual-cleanup-hook", $CleanupCmd, + "--preferred-challenges", "dns", + "--server", $Config.AcmeServer, + "--agree-tos", + "-d", $cert.domain, + "--non-interactive", + "--force-renewal", + "--disable-hook-validation" + ) + & certbot $CertbotArgs + + } else { + Write-Output " -> [INFO] Mode: STACKIT DNS Plugin (Direct Zone Update)" + # Execute Certbot using the official STACKIT DNS plugin + & certbot certonly --authenticator dns-stackit ` + --dns-stackit-project-id $Config.ProjectId ` + --dns-stackit-service-account $Config.DNS_SAKeyPath ` + --dns-stackit-propagation-seconds 120 ` + --server $Config.AcmeServer ` + --agree-tos -d $cert.domain --non-interactive --force-renewal + } + $certbotSuccess = ($LASTEXITCODE -eq 0) + + # --- END OF CERTBOT LOG BOUNDARY --- + Write-Output " ^==========================================================^" + Write-Output "" + + if (-not $certbotSuccess) { + Write-Error " [FAIL] Certbot process failed for $($cert.domain)." + $WorkflowHasErrors = $true + continue + } + } else { + # SkipCertbot mode: Assume files were generated out-of-band and just deploy them + Write-Output " -> [INFO] SkipCertbot enabled. Checking for existing local files..." + $certbotSuccess = (Test-Path $fullchainPath) -and (Test-Path $privkeyPath) + if (-not $certbotSuccess) { + Write-Error " [FAIL] Local certificate files not found for $($cert.domain)!" + $WorkflowHasErrors = $true + continue + } + } + + Write-Output " [+] Certificate generated successfully. Local files are ready." + + # Generate a unique name for the new certificate in STACKIT Certificate Manager + $safeDomain = $cert.domain -replace '\.', '-' + $newName = "auto-$($safeDomain)-$(Get-Date -Format 'yyyyMMdd-HHmm')" + + try { + # Reactivate ALB scope auth (in case Certbot/Hooks altered the active CLI session) + Write-Output " -> [Action] Ensuring STACKIT CLI Auth is active before upload..." + & stackit auth activate-service-account --service-account-key-path $Config.ALB_SAKeyPath + + Write-Output " -> [Action] Uploading new certificate '$newName' to STACKIT Certificate Manager in project '$($Config.ProjectId)'" + $newCert = New-StackitCertificate -ProjectId $Config.ProjectId -RegionId $Config.RegionId -CertName $newName -FullchainPath $fullchainPath -PrivkeyPath $privkeyPath -CertBaseUrl $Config.CertBaseUrl + Write-Output " [SUCCESS] Certificate successfully uploaded!" + + Write-Output " -> [Action] Patching ALB to use new Certificate ID: $($newCert.id)..." + $patch = Update-AlbListenerCert -ProjectId $Config.ProjectId -RegionId $Config.RegionId -AlbName $Config.AlbName -OldCertId $cert.certificateId -NewCertId $newCert.id -AlbBaseUrl $Config.AlbBaseUrl + Write-Output " [SUCCESS] ALB successfully updated with the new certificate!" + } catch { + Write-Error " [ERROR] STACKIT platform update failed for $($cert.domain): $($_.Exception.Message)" + $WorkflowHasErrors = $true + } +} + +if ($WorkflowHasErrors) { + Write-Error "`n=== Workflow Completed with ERRORS ===" + Write-Error "At least one certificate failed to renew or deploy. Please check the logs above." + exit 1 +} else { + Write-Output "`n=== Workflow Completed SUCCESSFULLY ===" + exit 0 +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Get-StackitAlbCertStatus_CLI.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Get-StackitAlbCertStatus_CLI.ps1 new file mode 100644 index 0000000..d7b3b26 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Get-StackitAlbCertStatus_CLI.ps1 @@ -0,0 +1,94 @@ +<# +.SYNOPSIS + Evaluates STACKIT ALB Certificate Expiration + +.DESCRIPTION + This helper script retrieves the current configuration of the STACKIT Application Load Balancer. + It extracts the IDs of all bound certificates, retrieves their public keys from the Certificate Manager, + and calculates the exact expiration dates using native X509 .NET classes. +#> +function Get-StackitAlbCertStatus { + param ( + [Parameter(Mandatory=$true)] [string]$ProjectId, + [Parameter(Mandatory=$true)] [string]$RegionId, + [Parameter(Mandatory=$true)] [string]$AlbName, + [Parameter(Mandatory=$false)] [string[]]$Whitelist = @(), + [Parameter(Mandatory=$false)] [int]$DaysWarning = 30, + [Parameter(Mandatory=$false)] [string]$AlbBaseUrl = "https://alb.api.stackit.cloud/v2", + [Parameter(Mandatory=$false)] [string]$CertBaseUrl = "https://certificates.api.stackit.cloud/v2", + [Parameter(Mandatory=$false)] [switch]$ForceRenew + ) + + $CertResults = New-Object System.Collections.Generic.List[PSCustomObject] + + try { + # 1. Fetch current ALB state + $AlbUrl = "$AlbBaseUrl/projects/$ProjectId/regions/$RegionId/load-balancers/$AlbName" + $Alb = stackit curl -X GET $AlbUrl | ConvertFrom-Json + + # 2. Iterate through listeners and extract bound certificate IDs + foreach ($Listener in $Alb.listeners) { + if ($Listener.protocol -eq "PROTOCOL_HTTPS" -and $Listener.https.certificateConfig.certificateIds) { + foreach ($HostRule in $Listener.http.hosts) { + + # Apply whitelist filtering if specified + if ($Whitelist.Count -eq 0 -or $Whitelist -contains $HostRule.host) { + foreach ($CertId in $Listener.https.certificateConfig.certificateIds) { + + # Bypass calculation if forced renewal is triggered + if ($ForceRenew) { + $CertResults.Add([PSCustomObject]@{ + domain = $HostRule.host + certificateId = $CertId + name = "Forced-Check" + expiryDate = "FORCED" + daysLeft = 0 + shouldReplace = $true + }) + continue + } + + # 3. Retrieve actual certificate details from STACKIT Certificate Manager + $CertUrl = "$CertBaseUrl/projects/$ProjectId/regions/$RegionId/certificates/$CertId" + try { + $CertJson = stackit curl -X GET $CertUrl | ConvertFrom-Json + + # Note: The STACKIT API returns the public key inside a JSON string. + # We must extract the raw PEM, strip headers, and sanitize it for Base64 conversion. + $rawCert = $CertJson.publicKey + $firstCertPart = ($rawCert -split "-----END CERTIFICATE-----")[0] + $cleanB64 = ($firstCertPart -replace "-----BEGIN CERTIFICATE-----", "") -replace '[^a-zA-Z0-9\+\/=]', '' + + # Pad Base64 string if necessary to avoid format exceptions + while ($cleanB64.Length % 4 -ne 0) { $cleanB64 += "=" } + + # Load the byte array into a .NET X509 object to reliably read the expiration date (NotAfter) + $certBytes = [System.Convert]::FromBase64String($cleanB64) + $x509 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certBytes) + + $CertResults.Add([PSCustomObject]@{ + domain = $HostRule.host + certificateId = $CertId + name = $CertJson.name + expiryDate = $x509.NotAfter.ToString("yyyy-MM-dd") + daysLeft = ($x509.NotAfter - (Get-Date)).Days + shouldReplace = (($x509.NotAfter - (Get-Date)).Days -lt $DaysWarning) + }) + } catch { + $CertResults.Add([PSCustomObject]@{ domain = $HostRule.host; error = "CLI Error processing $CertId" }) + } + } + } + } + } + } + + # Return unique certificates (a cert might be bound to multiple listeners) + return [PSCustomObject]@{ + projectId = $ProjectId; + albName = $AlbName; + certificates = $CertResults | Select-Object -Unique * } + } catch { + throw "ALB Request via STACKIT CLI failed: $($_.Exception.Message)" + } +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-CleanupHook.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-CleanupHook.ps1 new file mode 100644 index 0000000..3b08d71 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-CleanupHook.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS + Certbot Manual Cleanup Hook for STACKIT DNS Validation + +.DESCRIPTION + This script is invoked automatically by Certbot after the Let's Encrypt validation + attempt (whether successful or not). It ensures the temporary TXT challenge + records are removed from the STACKIT Verification Zone to maintain a clean DNS state. +#> + +$Domain = $env:CERTBOT_DOMAIN +$ProjectId = $env:STACKIT_PROJECT_ID +$SaKeyPath = $env:DNS_SA_KEY_PATH +$VerifyZoneFQDN = $env:VERIFY_ZONE_FQDN + +$ErrorActionPreference = "Stop" + +$MissingVars = @() +if ([string]::IsNullOrWhiteSpace($Domain)) { $MissingVars += "CERTBOT_DOMAIN" } +if ([string]::IsNullOrWhiteSpace($ProjectId)) { $MissingVars += "STACKIT_PROJECT_ID" } +if ([string]::IsNullOrWhiteSpace($VerifyZoneFQDN)) { $MissingVars += "VERIFY_ZONE_FQDN" } + +if ($MissingVars.Count -gt 0) { + [Console]::Error.WriteLine(">>> [HOOK FATAL] Missing required environment variables from Certbot/Main Script: $($MissingVars -join ', ')") + exit 1 +} + +try { + Write-Output ">>> [CLEANUP] Starting DNS Cleanup Hook for: $Domain" + $RecordName = $Domain + + # Suppress STACKIT CLI loading spinners from stderr to prevent Certbot logging errors + $authOutput = & stackit auth activate-service-account --service-account-key-path "$SaKeyPath" 2>&1 + if ($LASTEXITCODE -ne 0) { throw "STACKIT CLI Auth failed in Cleanup Hook." } + + $ZonesJson = & stackit dns zone list --project-id "$ProjectId" --output-format json 2>$null | ConvertFrom-Json + $Zone = $ZonesJson | Where-Object { $_.dnsName -eq "$VerifyZoneFQDN" -or $_.dnsName -eq "$VerifyZoneFQDN." } + if (-not $Zone) { throw "Verify Zone '$VerifyZoneFQDN' not found in STACKIT Project!" } + $ZoneId = $Zone.id + + Write-Output ">>> [CLEANUP] Fetching record sets for zone $ZoneId..." + $RecordsJson = & stackit dns record-set list --zone-id "$ZoneId" --project-id "$ProjectId" --output-format json 2>$null | ConvertFrom-Json + + # Account for FQDNs returned with or without a trailing dot + $ExpectedNameWithDot = "$RecordName.$VerifyZoneFQDN." + $ExpectedNameNoDot = "$RecordName.$VerifyZoneFQDN" + + $TargetRecord = $RecordsJson | Where-Object { + ($_.name -eq $ExpectedNameWithDot -or $_.name -eq $ExpectedNameNoDot) -and $_.type -eq "TXT" + } | Select-Object -First 1 + + if ($TargetRecord) { + $RecordId = $TargetRecord.id + Write-Output ">>> [CLEANUP] Found TXT record with ID '$RecordId'. Deleting..." + + # Suppress CLI auto-confirm warnings from stderr + $deleteOutput = & stackit dns record-set delete "$RecordId" --zone-id "$ZoneId" --project-id "$ProjectId" --assume-yes 2>&1 + + if ($LASTEXITCODE -ne 0) { + throw "Failed to delete record: $deleteOutput" + } + Write-Output ">>> [CLEANUP] Done." + } else { + Write-Output ">>> [CLEANUP] WARNING: TXT record for '$RecordName' not found. It may have been already deleted." + } + +} catch { + [Console]::Error.WriteLine(">>> [CLEANUP ERROR] $($_.Exception.Message)") + exit 1 +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-DnsHook.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-DnsHook.ps1 new file mode 100644 index 0000000..1707f32 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-DnsHook.ps1 @@ -0,0 +1,75 @@ +<# +.SYNOPSIS + Certbot Manual Auth Hook for STACKIT DNS Validation + +.DESCRIPTION + This script is invoked automatically by Certbot during the DNS-01 challenge. + It takes the validation token provided by Let's Encrypt and creates a temporary + TXT record in a designated STACKIT Verification Zone (Delegation Mode). +#> + +# Certbot automatically injects these environment variables +$Domain = $env:CERTBOT_DOMAIN +$ValidationToken = $env:CERTBOT_VALIDATION + +# Variables passed down from the Main Orchestrator script +$ProjectId = $env:STACKIT_PROJECT_ID +$SaKeyPath = $env:DNS_SA_KEY_PATH +$VerifyZoneFQDN = $env:VERIFY_ZONE_FQDN + +$ErrorActionPreference = "Stop" + +# Ensure all required inputs are present +$MissingVars = @() +if ([string]::IsNullOrWhiteSpace($Domain)) { $MissingVars += "CERTBOT_DOMAIN" } +if ([string]::IsNullOrWhiteSpace($ProjectId)) { $MissingVars += "STACKIT_PROJECT_ID" } +if ([string]::IsNullOrWhiteSpace($VerifyZoneFQDN)) { $MissingVars += "VERIFY_ZONE_FQDN" } + +if ($MyInvocation.MyCommand.Name -match "DnsHook" -and [string]::IsNullOrWhiteSpace($ValidationToken)) { + $MissingVars += "CERTBOT_VALIDATION" +} + +if ($MissingVars.Count -gt 0) { + # We use [Console]::Error.WriteLine to ensure Certbot directly catches fatal hook errors + [Console]::Error.WriteLine(">>> [HOOK FATAL] Missing required environment variables from Certbot/Main Script: $($MissingVars -join ', ')") + exit 1 +} + +try { + Write-Output ">>> [HOOK] Starting DNS Auth Hook for: $Domain" + $RecordName = $Domain + + # Authenticate via STACKIT CLI using the dedicated DNS Service Account. + # CRITICAL: We redirect stream 2 (`2>&1`) to a variable. The CLI may output loading spinners + # to stderr, which Certbot falsely interprets as a critical script failure. + $authOutput = & stackit auth activate-service-account --service-account-key-path "$SaKeyPath" 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "STACKIT CLI Auth failed in Hook: $authOutput" + } + + # Fetch the ID of the Verification Zone. + # `2>$null` suppresses CLI warnings to ensure ConvertFrom-Json receives a clean JSON payload. + $ZonesJson = & stackit dns zone list --project-id "$ProjectId" --output-format json 2>$null | ConvertFrom-Json + $Zone = $ZonesJson | Where-Object { $_.dnsName -eq "$VerifyZoneFQDN" -or $_.dnsName -eq "$VerifyZoneFQDN." } + if (-not $Zone) { + throw "Verify Zone '$VerifyZoneFQDN' not found in STACKIT Project!" + } + $ZoneId = $Zone.id + + Write-Output ">>> [HOOK] Creating TXT Record: '$RecordName' in Zone: '$VerifyZoneFQDN'" + $QuotedToken = "`"$ValidationToken`"" + + # Create the TXT record. Stream 2 is captured again to suppress CLI auto-confirm warnings. + $createOutput = & stackit dns record-set create --zone-id "$ZoneId" --name "$RecordName" --type "TXT" --record "$QuotedToken" --project-id "$ProjectId" --assume-yes 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "Failed to create TXT record: $createOutput" + } + + # Let's Encrypt requires time for the DNS entry to propagate across global nameservers + Write-Output ">>> [HOOK] Sleeping 30 seconds to allow DNS propagation..." + Start-Sleep -Seconds 30 + +} catch { + [Console]::Error.WriteLine(">>> [HOOK ERROR] $($_.Exception.Message)") + exit 1 +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/StackitHelper_CLI.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/StackitHelper_CLI.ps1 new file mode 100644 index 0000000..bc69f39 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/StackitHelper_CLI.ps1 @@ -0,0 +1,127 @@ +<# +.SYNOPSIS + STACKIT API Interaction Helpers + +.DESCRIPTION + Provides functions to interact with the STACKIT Certificate Manager and the + Application Load Balancer API to deploy newly generated certificates. +#> + +function New-StackitCertificate { + <# + .SYNOPSIS + Uploads a local PEM certificate and private key to STACKIT Certificate Manager. + #> + param ($ProjectId, $RegionId, $CertName, $FullchainPath, $PrivkeyPath, $CertBaseUrl) + + # Read the PEM files and normalize line endings (CRLF to LF) for API compatibility + $cert = (Get-Content -Raw $FullchainPath) -replace "`r`n", "`n" + $key = (Get-Content -Raw $PrivkeyPath) -replace "`r`n", "`n" + + # Construct the JSON payload expected by STACKIT + $body = @{ + name = $CertName + publicKey = $cert.Trim() + privateKey = $key.Trim() + } | ConvertTo-Json + + $url = "$CertBaseUrl/projects/$ProjectId/regions/$RegionId/certificates" + + # Store payload in a temporary file to safely pass it to the curl wrapper + $tempFile = New-TemporaryFile + + try { + # Ensure UTF8 encoding without Byte Order Mark (BOM) to prevent parsing errors + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($tempFile.FullName, $body, $utf8NoBom) + + $response = stackit curl -X POST $url --data "@$($tempFile.FullName)" + + if ($LASTEXITCODE -ne 0) { + throw "Failed to upload new certificate '$CertName' to STACKIT Certificate Manager. API Output: $response" + } + + return $response | ConvertFrom-Json + } + catch { + Write-Error "[ERROR] New-StackitCertificate: $($_.Exception.Message)" + throw + } + finally { + if (Test-Path $tempFile.FullName) { + Remove-Item -Path $tempFile.FullName -Force + } + } +} + +function Update-AlbListenerCert { + <# + .SYNOPSIS + Patches an existing Application Load Balancer to use a newly uploaded certificate ID. + #> + param ($ProjectId, $RegionId, $AlbName, $OldCertId, $NewCertId, $AlbBaseUrl) + + $url = "$AlbBaseUrl/projects/$ProjectId/regions/$RegionId/load-balancers/$AlbName" + + # 1. Retrieve the complete, current state of the ALB + $alb = stackit curl -X GET $url | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { + throw "Failed to retrieve current configuration for ALB '$AlbName' from STACKIT API." + } + + # 2. Iterate through listeners and replace the old Certificate ID with the new one + $changed = $false + foreach ($l in $alb.listeners) { + if ($l.protocol -eq "PROTOCOL_HTTPS" -and $l.https.certificateConfig) { + $ids = $l.https.certificateConfig.certificateIds + for ($i=0; $i -lt $ids.Count; $i++) { + if ($ids[$i] -eq $OldCertId) { + $ids[$i] = $NewCertId + $changed = $true + } + } + } + } + + # 3. If modifications were made, we must push the entire state back via PUT + if ($changed) { + + # CRITICAL: The GET request returns read-only state variables. + # Sending these back in a PUT request will cause the STACKIT API to reject the payload. + # We must explicitly strip them from the object. + $alb.PSObject.Properties.Remove("status") + $alb.PSObject.Properties.Remove("loadBalancerSecurityGroup") + $alb.PSObject.Properties.Remove("targetSecurityGroup") + + if ($alb.options -and $alb.options.ephemeralAddress -eq $true) { + $alb.PSObject.Properties.Remove("externalAddress") + } + + # Convert back to JSON, ensuring nested arrays (like listeners) are not truncated + $body = $alb | ConvertTo-Json -Depth 20 + $tempFile = New-TemporaryFile + + try { + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($tempFile.FullName, $body, $utf8NoBom) + + $response = stackit curl -X PUT $url --data "@$($tempFile.FullName)" + + if ($LASTEXITCODE -ne 0) { + throw "Failed to update listener configuration for ALB '$AlbName' with new Certificate ID '$NewCertId'. API Output: $response" + } + return $true + } + catch { + Write-Error "[ERROR] Update-AlbListenerCert: $($_.Exception.Message)" + throw + } + finally { + if (Test-Path $tempFile.FullName) { + Remove-Item -Path $tempFile.FullName -Force + } + } + } + + return $false +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_detail_level.png b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_detail_level.png new file mode 100644 index 0000000..e2b5061 Binary files /dev/null and b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_detail_level.png differ diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_high_level.png b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_high_level.png new file mode 100644 index 0000000..6acbfa4 Binary files /dev/null and b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_high_level.png differ diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_detail_level.png b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_detail_level.png new file mode 100644 index 0000000..de227cd Binary files /dev/null and b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_detail_level.png differ diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_high_level.png b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_high_level.png new file mode 100644 index 0000000..987592f Binary files /dev/null and b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_high_level.png differ diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/templates/cloud-init.yaml.tpl b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/templates/cloud-init.yaml.tpl new file mode 100644 index 0000000..6b0adaf --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/templates/cloud-init.yaml.tpl @@ -0,0 +1,39 @@ +#cloud-config +# Docker host bootstrap for ALB + Certbot / ACME DNS-01 workshop +# Target OS: Debian 12 (bookworm) +# Rendered via Terraform templatefile() — variables: start_nginx_test_container + +package_update: true +package_upgrade: false +packages: + - ca-certificates + - curl + - gnupg + - apt-transport-https + +runcmd: + # Add Docker's official GPG key + - install -m 0755 -d /etc/apt/keyrings + - curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc + - chmod a+r /etc/apt/keyrings/docker.asc + # Add Docker apt repository for Debian + - >- + . /etc/os-release && + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] + https://download.docker.com/linux/debian $${VERSION_CODENAME} stable" + | tee /etc/apt/sources.list.d/docker.list > /dev/null + - apt-get update -y + # Install Docker Engine + Compose plugin + - >- + DEBIAN_FRONTEND=noninteractive apt-get install -y + docker-ce docker-ce-cli containerd.io + docker-buildx-plugin docker-compose-plugin + - systemctl enable --now docker + # Add default Debian cloud user to docker group + - usermod -aG docker debian +%{ if start_nginx_test_container ~} + # Start nginx test container — verify with: curl http:// + - docker run -d --name nginx-test --restart unless-stopped -p 80:80 nginx:alpine +%{ endif ~} + +final_message: "Cloud-init complete after $UPTIME seconds. Docker host is ready." diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/.gitignore b/examples/alb-tls-examples/vm-alb-self-signed-cert/.gitignore new file mode 100644 index 0000000..5d20f84 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/.gitignore @@ -0,0 +1,20 @@ +**/.terraform/* +*.tfstate +*.tfstate.* +*.tfvars +*.tfvars.json +.terraform.tfstate.lock.info +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.terraformrc +terraform.rc +keys/* +backend.conf +certs/ +.DS_Store +*.bkp +.idea diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform-version b/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform-version new file mode 100644 index 0000000..22708fe --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform-version @@ -0,0 +1 @@ +v1.5.7 diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform.lock.hcl b/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform.lock.hcl new file mode 100644 index 0000000..adbccc5 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.3.0" + constraints = "~> 4.0" + hashes = [ + "h1:5bCU/c+2HUh7GhclzNSH6gAuoCS4inW3obEtRAwu6WQ=", + "zh:0ab58d6f8991d436c7d2dbd89ed814709b949b07ac5a54ee53b0aec1fa772a8b", + "zh:60b347abcb56f45d97c56f14d895069cd15a83993f199777f571b79fea3642ee", + "zh:6889be32640349230de3f23856e6f04e0e9ced4a84a27d3f552fa54684448218", + "zh:73f8e1ecf7135033165fb14b7e8bf4d656f3ce13065ec35762ea0481975328c7", + "zh:94ce25ee253eca0b42cae9c856b36bca8103b6453012d1b279c3623c805f2d42", + "zh:96bc6de9fd67bc446fd11257872e1ffb1029a996ed1d65a3f6b43f6d408ad9ab", + "zh:97c609a310a51bfd504d704e036d72064a84bf0bdb36cc08cd4cc66098212b41", + "zh:a12c16e94533c5bd123f75032576b9dc91dd5d5ccd5f7cf331d0f2e1adc55cf8", + "zh:c4f014f876adf7af57188795050bda5b0029d8c7d7773031102b6c36dcf1fc21", + "zh:d9b0a21583aaa3df3a95394fb949a3c515ff71c2ff5a1fc4a73d364aa90bfca5", + "zh:da510d22f0c6d71ad19a76406f106b782448f512375787ecfabb338ed1e311a7", + "zh:f0e9447a9ce3a24cdaa113089e65663c836d8b9bfdb915a1c0284e0112cab5c0", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/stackitcloud/stackit" { + version = "0.98.0" + constraints = ">= 0.98.0" + hashes = [ + "h1:/FB0wBnvmjumjykX+j90kSck6LMScDaYo1STO5Vp/kw=", + "zh:031028340fbaeeb5c4c6b1d5c6d6287a70cf253cfb89f04d462a1c0ab6237ffc", + "zh:0dde99e7b343fa01f8eefc378171fb8621bedb20f59157d6cc8e3d46c738105f", + "zh:0eee18f9a262fa58966c960f1f0863eed92cd953d0f0306ecc456b58cc2911f8", + "zh:1646966ebac0eb5d6c78ac5aa1528921d7a635f14d81300463a402c55e33cfd3", + "zh:5374ab9e5e6d837787b4f18bcf0125a1bf3ee2da40c022cc7695d6879fed111b", + "zh:6a5b9e1307055f8d358373da625ffcb4d77ec44f260d14473b10e5777380765e", + "zh:6c90090504474695ab7290d64386dd988f4fb65c90c74c9cf3a6da6226ae8a70", + "zh:8317218828f29be95ce712863646dc8968e146ec14e5ab258cb1e8f8b649245b", + "zh:9eef08e4fb7a75760f9dc8a422446f19a210ebf8177dd5aeb97444295f0120cf", + "zh:9f2147eee63feae75b96f17f3b3ebab8a29cd7164cdd08eb2bb871e5c425a77f", + "zh:b63ea754eea233292fb73d87a9810104da2bd347abf2ca0da44ac76591dcdddb", + "zh:de60bd928828a836e446f9f89e7a3bfc4e6dd73bac6827914087b34e4ad0c978", + "zh:f22d295b2e4e94ae1566e20fd752825e008a62250cf7243f1161c0bf4e986518", + "zh:f7e57bc7be2cc016983ff3ad50d2733b85e90bfaa7aa9e2192563dc9d422fb07", + ] +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/00-backend.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/00-backend.tf new file mode 100644 index 0000000..dfc2f09 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/00-backend.tf @@ -0,0 +1,17 @@ +# 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 { + backend "s3" {} +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/00-provider.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/00-provider.tf new file mode 100644 index 0000000..44c0dc7 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/00-provider.tf @@ -0,0 +1,34 @@ +# 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 { + required_version = ">= 1.3.0" + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.98.0" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } +} + +provider "stackit" { + default_region = var.stackit_region + service_account_key_path = var.stackit_service_account_key_path + # required for stackit_application_load_balancer (beta resource) + enable_beta_resources = true +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/01-variables.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/01-variables.tf new file mode 100644 index 0000000..6af7b73 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/01-variables.tf @@ -0,0 +1,176 @@ +# 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. + +# ─── Provider ───────────────────────────────────────────────────────────────── + +variable "stackit_region" { + description = "STACKIT region, e.g. eu01" + type = string + default = "eu01" +} + +variable "stackit_service_account_key_path" { + description = "Path to the STACKIT service account key JSON file" + type = string + default = "keys/sa-key.json" +} + +# ─── Resource Hierarchy ─────────────────────────────────────────────────────── + +variable "organization_id" { + description = "STACKIT organisation container ID — find it in the Portal under Organisation → Settings" + type = string +} + +variable "owner_email" { + description = "Email of the resource owner; must be an existing STACKIT user in the organisation" + type = string +} + +variable "folder_name" { + description = "Display name of the folder (must match the existing folder name when importing)" + type = string + default = "alb-showcase" +} + +variable "project_name" { + description = "Name of the new project to create inside the folder" + type = string + default = "vm-alb-self-signed-cert" +} + +# ─── Naming ─────────────────────────────────────────────────────────────────── + +variable "name_prefix" { + description = "Short prefix applied to all resource names (network, VMs, ALB, certificate)" + type = string + default = "vm-alb-tls" +} + +# ─── Network ────────────────────────────────────────────────────────────────── + +variable "network_cidr" { + description = "IPv4 CIDR block for the private network, e.g. 10.10.0.0/24" + type = string + default = "10.10.0.0/24" + + validation { + condition = can(cidrnetmask(var.network_cidr)) + error_message = "network_cidr must be a valid CIDR, e.g. 10.10.0.0/24." + } +} + +variable "admin_cidr" { + description = "Source CIDR for SSH access (port 22). Use your own egress IP, e.g. 203.0.113.10/32. Avoid 0.0.0.0/0." + type = string + + validation { + condition = can(cidrnetmask(var.admin_cidr)) + error_message = "admin_cidr must be a valid CIDR, e.g. 203.0.113.10/32." + } +} + +# ─── Compute / VM ───────────────────────────────────────────────────────────── + +variable "machine_type" { + description = "STACKIT machine type for backend VMs — list available: stackit server machine-type list --project-id " + type = string + default = "g1.1" +} + +variable "availability_zone" { + description = "Availability zone for the VMs, e.g. eu01-1, eu01-2, eu01-3" + type = string + default = "eu01-1" +} + +variable "image_id" { + description = "UUID of the boot image (Debian 12 recommended) — list: stackit image list --all --project-id " + type = string +} + +variable "boot_volume_size_gb" { + description = "Root disk size in GB for each backend VM" + type = number + default = 20 + + validation { + condition = var.boot_volume_size_gb >= 10 + error_message = "boot_volume_size_gb must be at least 10 GB." + } +} + +variable "keypair_name" { + description = "Name of the SSH key pair to register in STACKIT" + type = string + default = "vm-alb-tls-key" +} + +variable "ssh_public_key" { + description = "SSH public key string (ssh-ed25519 AAAA... or ssh-rsa AAAA...) — never commit the private key" + type = string + sensitive = true +} + +# ─── TLS / Certificate ──────────────────────────────────────────────────────── + +variable "tls_common_name" { + description = "Common Name (CN) for the self-signed certificate, e.g. alb.example.com or the ALB IP address" + type = string + default = "alb.example.internal" +} + +variable "tls_organization" { + description = "Organisation name embedded in the certificate subject" + type = string + default = "STACKIT ALB Showcase" +} + +variable "tls_validity_hours" { + description = "Certificate validity in hours (8760 = 1 year)" + type = number + default = 8760 +} + +# ─── ALB ────────────────────────────────────────────────────────────────────── + +variable "alb_plan_id" { + description = "ALB service plan — p10 is the smallest available plan" + type = string + default = "p10" +} + +variable "alb_acl_ranges" { + description = "Source CIDRs allowed to reach the ALB. Use [\"0.0.0.0/0\"] for public access." + type = list(string) + default = ["0.0.0.0/0"] +} + +# ─── DNS ────────────────────────────────────────────────────────────────────── + +variable "dns_zone_name" { + description = "Human-readable label for the DNS zone resource" + type = string + default = "vm-alb-tls-zone" +} + +variable "dns_name" { + description = "DNS zone apex FQDN, e.g. vm-alb-tls.stackit.gg" + type = string +} + +variable "dns_contact_email" { + description = "SOA contact email for the DNS zone" + type = string +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/02-resource-hierarchy.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/02-resource-hierarchy.tf new file mode 100644 index 0000000..935f925 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/02-resource-hierarchy.tf @@ -0,0 +1,43 @@ +# 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. + +locals { + common_labels = { + environment = "showcase" + managed-by = "terraform" + use-case = "vm-alb-self-signed-tls" + } +} + +resource "stackit_resourcemanager_folder" "showcase" { + name = var.folder_name + owner_email = var.owner_email + parent_container_id = var.organization_id + + labels = { + managed-by = "terraform" + } + + lifecycle { + ignore_changes = [labels] + } +} + +resource "stackit_resourcemanager_project" "showcase" { + name = var.project_name + owner_email = var.owner_email + parent_container_id = stackit_resourcemanager_folder.showcase.id + + labels = local.common_labels +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/03-network.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/03-network.tf new file mode 100644 index 0000000..c17ddcd --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/03-network.tf @@ -0,0 +1,84 @@ +# 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" "main" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = "${var.name_prefix}-network" + ipv4_prefix = var.network_cidr + ipv4_nameservers = ["8.8.8.8", "1.1.1.1"] + routed = true + + labels = local.common_labels +} + +resource "stackit_security_group" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = "${var.name_prefix}-vm-sg" + description = "Security group for the showcase VM" + stateful = true + + labels = local.common_labels +} + +resource "stackit_security_group_rule" "ssh_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "SSH from admin CIDR only" + + protocol = { name = "tcp" } + port_range = { min = 22, max = 22 } + ip_range = var.admin_cidr +} + +resource "stackit_security_group_rule" "http_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "HTTP port 80 — ALB forwards traffic here after TLS termination" + + protocol = { name = "tcp" } + port_range = { min = 80, max = 80 } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_tcp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow all outbound TCP (Docker image pulls, apt)" + + protocol = { name = "tcp" } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_udp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow all outbound UDP (DNS)" + + protocol = { name = "udp" } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_icmp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow outbound ICMP" + + protocol = { name = "icmp" } + ip_range = "0.0.0.0/0" +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/04-compute.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/04-compute.tf new file mode 100644 index 0000000..4170e38 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/04-compute.tf @@ -0,0 +1,67 @@ +# 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_key_pair" "showcase" { + name = var.keypair_name + public_key = chomp(var.ssh_public_key) + + labels = local.common_labels +} + +resource "stackit_network_interface" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + network_id = stackit_network.main.network_id + name = "${var.name_prefix}-vm-nic" + security_group_ids = [stackit_security_group.vm.security_group_id] + + # The ALB injects its own target security group into this NIC after creation. + lifecycle { + ignore_changes = [security_group_ids] + } +} + +resource "stackit_public_ip" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + network_interface_id = stackit_network_interface.vm.network_interface_id + + labels = local.common_labels +} + +resource "stackit_server" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = "${var.name_prefix}-vm" + machine_type = var.machine_type + availability_zone = var.availability_zone + keypair_name = stackit_key_pair.showcase.name + user_data = file("${path.root}/templates/cloud-init.yaml.tpl") + + boot_volume = { + source_type = "image" + source_id = var.image_id + size = var.boot_volume_size_gb + } + + network_interfaces = [stackit_network_interface.vm.network_interface_id] + + agent = { + provisioning_policy = "ALWAYS" + } + + labels = local.common_labels + + depends_on = [ + stackit_network_interface.vm, + stackit_key_pair.showcase, + ] +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/05-certificate.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/05-certificate.tf new file mode 100644 index 0000000..9d0e162 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/05-certificate.tf @@ -0,0 +1,44 @@ +# 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 "tls_private_key" "self_signed" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "self_signed" { + private_key_pem = tls_private_key.self_signed.private_key_pem + + subject { + common_name = var.tls_common_name + organization = var.tls_organization + } + + dns_names = [var.tls_common_name] + validity_period_hours = var.tls_validity_hours + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth", + ] +} + +resource "stackit_alb_certificate" "self_signed" { + project_id = stackit_resourcemanager_project.showcase.project_id + region = var.stackit_region + name = "${var.name_prefix}-selfsigned-cert" + public_key = tls_self_signed_cert.self_signed.cert_pem + private_key = tls_private_key.self_signed.private_key_pem +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/05-dns.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/05-dns.tf new file mode 100644 index 0000000..9d24069 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/05-dns.tf @@ -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_dns_zone" "showcase" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = var.dns_zone_name + dns_name = var.dns_name + contact_email = var.dns_contact_email + type = "primary" + default_ttl = 300 +} + +resource "stackit_dns_record_set" "alb_a" { + project_id = stackit_resourcemanager_project.showcase.project_id + zone_id = stackit_dns_zone.showcase.zone_id + name = var.dns_name + type = "A" + ttl = 300 + records = [stackit_public_ip.alb.ip] +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/06-alb.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/06-alb.tf new file mode 100644 index 0000000..14cb37f --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/06-alb.tf @@ -0,0 +1,96 @@ +# 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_public_ip" "alb" { + project_id = stackit_resourcemanager_project.showcase.project_id + + labels = local.common_labels + + lifecycle { + ignore_changes = [network_interface_id] + } +} + +resource "stackit_application_load_balancer" "main" { + project_id = stackit_resourcemanager_project.showcase.project_id + region = var.stackit_region + name = "${var.name_prefix}-alb" + plan_id = var.alb_plan_id + external_address = stackit_public_ip.alb.ip + + networks = [ + { + network_id = stackit_network.main.network_id + role = "ROLE_LISTENERS_AND_TARGETS" + } + ] + + listeners = [ + { + name = "https" + port = 443 + protocol = "PROTOCOL_HTTPS" + http = { + hosts = [ + { + host = "*" + rules = [{ target_pool = "${var.name_prefix}-pool" }] + } + ] + } + https = { + certificate_config = { + certificate_ids = [stackit_alb_certificate.self_signed.cert_id] + } + } + }, + { + name = "http" + port = 80 + protocol = "PROTOCOL_HTTP" + http = { + hosts = [ + { + host = "*" + rules = [{ target_pool = "${var.name_prefix}-pool" }] + } + ] + } + } + ] + + target_pools = [ + { + name = "${var.name_prefix}-pool" + target_port = 80 + targets = [ + { + display_name = "${var.name_prefix}-vm" + ip = stackit_network_interface.vm.ipv4 + } + ] + } + ] + + options = { + private_network_only = false + access_control = { + allowed_source_ranges = var.alb_acl_ranges + } + } + + labels = local.common_labels + + depends_on = [stackit_network_interface.vm] +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/07-outputs.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/07-outputs.tf new file mode 100644 index 0000000..61f8672 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/07-outputs.tf @@ -0,0 +1,73 @@ +# 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. + +output "project_id" { + description = "UUID of the created project" + value = stackit_resourcemanager_project.showcase.project_id +} + +output "alb_public_ip" { + description = "Public IPv4 address of the Application Load Balancer" + value = stackit_public_ip.alb.ip +} + +output "alb_https_url" { + description = "HTTPS URL — certificate is self-signed, use curl -k or accept the browser warning" + value = "https://${stackit_public_ip.alb.ip}" +} + +output "alb_http_url" { + description = "Plain HTTP URL for baseline connectivity tests" + value = "http://${stackit_public_ip.alb.ip}" +} + +output "vm_public_ip" { + description = "Public IPv4 address of the VM (for SSH access)" + value = stackit_public_ip.vm.ip +} + +output "vm_private_ip" { + description = "Private IPv4 address of the VM" + value = stackit_network_interface.vm.ipv4 +} + +output "ssh_command" { + description = "SSH command to connect to the VM (Debian 12 default user)" + value = "ssh debian@${stackit_public_ip.vm.ip}" +} + +output "certificate_id" { + description = "STACKIT ALB certificate ID referenced by the HTTPS listener" + value = stackit_alb_certificate.self_signed.cert_id +} + +output "certificate_expiry" { + description = "Certificate expiry timestamp (UTC)" + value = tls_self_signed_cert.self_signed.validity_end_time +} + +output "dns_name" { + description = "DNS name pointing to the ALB" + value = var.dns_name +} + +output "dns_nameservers" { + description = "Nameservers for the DNS zone — set these at your registrar to delegate the zone" + value = stackit_dns_zone.showcase.primary_name_server +} + +output "curl_test" { + description = "curl command to test the HTTPS endpoint by DNS name" + value = "curl -k https://${var.dns_name}" +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/README.md b/examples/alb-tls-examples/vm-alb-self-signed-cert/README.md new file mode 100644 index 0000000..1b4d705 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/README.md @@ -0,0 +1,259 @@ +# vm-alb-self-signed-cert + +Introductory showcase: a STACKIT VM with Docker nginx, an Application Load Balancer (ALB) in front of it, and a self-signed TLS certificate — fully managed by Terraform, no external tools, no ACME, no Kubernetes. + +--- + +## Architecture + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant TLS as hashicorp/tls + + Note over User,TLS: terraform apply + + User->>TF: create Folder + Project + TF-->>User: project_id + + User->>TF: create Network + Security Group + TF-->>User: network_id + + User->>TLS: generate RSA Private Key + TLS-->>User: private_key_pem + + User->>TLS: sign Self-Signed Certificate (private_key_pem) + TLS-->>User: cert_pem + + User->>TF: upload ALB Certificate (cert_pem + private_key_pem) + TF-->>User: cert_id + + User->>TF: create VM (cloud-init → Docker · nginx) + TF-->>User: private_ip · vm_public_ip + + User->>TF: create Public IP + DNS Zone + A-Record + TF-->>User: alb_public_ip · zone ready + + User->>TF: create ALB (cert_id · private_ip · alb_public_ip) + TF-->>User: ✅ https://your-domain ready +``` + +``` +STACKIT Organisation +└── Folder: alb-showcase + └── Project: vm-alb-self-signed-cert + ├── Network: vm-alb-tls-network (10.10.0.0/24, routed) + │ └── Security Group: vm-alb-tls-vm-sg + │ ├── Ingress TCP 22 ← admin_cidr (SSH) + │ ├── Ingress TCP 80 ← 0.0.0.0/0 (ALB backend traffic) + │ └── Egress all → 0.0.0.0/0 + ├── VM: vm-alb-tls-vm (Debian 12) + │ └── Docker → nginx:alpine :80 + ├── DNS Zone: vm-alb-tls.stackit.gg + │ └── A-Record → ALB Public IP + └── ALB: vm-alb-tls-alb + ├── Public IP (static) + ├── Certificate: vm-alb-tls-selfsigned-cert + ├── Listener HTTP :80 → vm-alb-tls-pool + └── Listener HTTPS :443 → vm-alb-tls-pool (TLS terminated) +``` + +--- + +## Overview + +| Component | Description | +| ------------------ | ---------------------------------------------------------------------------------- | +| Resource hierarchy | Folder + Project under an existing STACKIT organisation | +| Network | Private routed network (`10.10.0.0/24`) with security groups | +| Compute | Debian 12 VM with Docker Engine + nginx:alpine | +| Certificate | RSA key + self-signed X.509 cert generated by `hashicorp/tls`, uploaded to STACKIT | +| DNS | Primary zone + A-record pointing to the ALB public IP | +| Load Balancer | ALB with HTTPS :443 (TLS terminates here) and HTTP :80 | + +| In this showcase | Not in this showcase | +| ----------------------------- | --------------------------- | +| Self-signed TLS via Terraform | Let's Encrypt / ACME | +| One VM + Docker nginx | Multiple VMs / auto-scaling | +| STACKIT DNS Zone + A-Record | Certificate renewal | +| Fully Terraform-managed | Kubernetes / cert-manager | + +→ For Let's Encrypt: [`vm-alb-certbot-letsencrypt/`](../vm-alb-certbot-letsencrypt/README.md) +→ For Kubernetes: [`alb-k8s/`](../alb-k8s/README.md) + +--- + +## Prerequisites + +| Tool | Version | +| ----------- | -------- | +| Terraform | >= 1.5.7 | +| STACKIT CLI | latest | +| SSH client | — | + +### Required STACKIT permissions + +| Service | Role | +| --------------------------------- | -------- | +| Resource Manager (Folder/Project) | `editor` | +| Compute | `editor` | +| Networking | `editor` | +| DNS | `editor` | +| Application Load Balancer | `editor` | + +### Required variables + +| Variable | Description | Example | +| ------------------- | ---------------------------------- | -------------------------------- | +| `organization_id` | STACKIT organisation container ID | Portal → Organisation → Settings | +| `owner_email` | Owner email for folder and project | `name@example.com` | +| `folder_name` | Name of the existing folder | `alb-showcase` | +| `image_id` | UUID of the Debian 12 boot image | `stackit image list --all` | +| `ssh_public_key` | SSH public key string | `ssh-ed25519 AAAA...` | +| `admin_cidr` | Your IP for SSH access | `203.0.113.10/32` | +| `dns_name` | DNS name for the ALB | `vm-alb-tls.stackit.gg` | +| `dns_contact_email` | SOA contact for the DNS zone | `name@example.com` | + +All other variables have sensible defaults — see [`01-variables.tf`](01-variables.tf). + +--- + +## Deployment + +### 1. Configure variables + +```bash +cp examples/terraform.tfvars.example terraform.tfvars +# Fill in: organization_id, owner_email, image_id, ssh_public_key, admin_cidr, dns_name +``` + +Find your egress IP for `admin_cidr`: + +```bash +curl -s https://ifconfig.schwarz +``` + +Find available Debian 12 image UUIDs: + +```bash +stackit image list --all --project-id +``` + +### 2. Configure remote state + +```bash +cp examples/backend.conf.example backend.conf +# Fill in: bucket, key, access_key, secret_key +``` + +### 3. Deploy + +```bash +terraform init -backend-config=backend.conf +terraform plan +terraform apply +``` + +Duration: ~5–8 minutes. Expected resources: ~15. + +### 4. Outputs + +```bash +terraform output +``` + +--- + +## Validation + +```bash +# HTTPS — -k skips the self-signed cert warning +curl -k https://vm-alb-tls.stackit.gg + +# HTTP +curl http://vm-alb-tls.stackit.gg + +# Inspect TLS certificate +openssl s_client -connect vm-alb-tls.stackit.gg:443 /dev/null \ + | openssl x509 -noout -subject -issuer -dates + +# SSH to the VM +ssh debian@$(terraform output -raw vm_public_ip) +docker ps +docker logs nginx +``` + +--- + +## File Structure + +``` +vm-alb-self-signed-cert/ +├── .terraform-version # Terraform version pin (v1.5.7) +├── .gitignore +├── 00-backend.tf # S3 remote state (STACKIT Object Storage) +├── 00-provider.tf # stackitcloud/stackit + hashicorp/tls +├── 01-variables.tf # All variables with descriptions and defaults +├── 02-resource-hierarchy.tf # locals, folder resource, project +├── 03-network.tf # Network, security group, rules +├── 04-compute.tf # SSH key pair, NIC, VM (Docker nginx) +├── 05-certificate.tf # tls_private_key, tls_self_signed_cert, stackit_alb_certificate +├── 05-dns.tf # DNS zone + A-record +├── 06-alb.tf # Public IP, Application Load Balancer +├── 07-outputs.tf # All outputs (IP, URLs, SSH, cert_id, curl) +├── templates/ +│ └── cloud-init.yaml.tpl # Docker Engine install + nginx:alpine container +├── examples/ +│ ├── terraform.tfvars.example +│ └── backend.conf.example +├── docs/ +│ └── architecture.md # Deployment sequence diagram + component overview +└── keys/ # SA key JSON — gitignored +``` + +--- + +## Security + +| File | Git status | Contains | +| ------------------ | ---------- | -------------------------- | +| `terraform.tfvars` | gitignored | SSH key, sensitive config | +| `backend.conf` | gitignored | Object Storage access keys | +| `keys/` | gitignored | Service account JSON key | + +- SSH access is restricted to `admin_cidr` — never use `0.0.0.0/0` +- The TLS private key is stored in Terraform state — acceptable for a showcase, use KMS/Vault for production + +--- + +## Cleanup + +```bash +terraform destroy +``` + +Removes all resources: ALB, VM, network, certificate, DNS zone, public IP, project. +The parent folder is not destroyed (it pre-exists and was imported into state). + +--- + +## Troubleshooting + +| Problem | Cause | Fix | +| ------------------------------- | ------------------------------- | ------------------------------------------------ | +| `curl: (7) Failed to connect` | ALB not ready yet | Wait 1–2 min | +| `curl: (35) SSL error` | Self-signed cert | Use `-k` flag | +| nginx not responding | Docker image pull still running | Wait 3–5 min, check `docker ps` on VM | +| DNS not resolving | Zone not yet propagated | Run `dig vm-alb-tls.stackit.gg` and wait | +| Provider error on folder labels | Existing labels after import | `lifecycle { ignore_changes = [labels] }` is set | + +--- + +## References + +- [Full architecture details](docs/architecture.md) +- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) +- [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) +- [STACKIT Developer Documentation](https://docs.stackit.cloud) +- [hashicorp/tls Provider](https://registry.terraform.io/providers/hashicorp/tls/latest/docs) diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/docs/architecture.md b/examples/alb-tls-examples/vm-alb-self-signed-cert/docs/architecture.md new file mode 100644 index 0000000..1ab2fb1 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/docs/architecture.md @@ -0,0 +1,128 @@ +# Architecture: vm-alb-self-signed-cert + +## Deployment Sequence + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant TLS as hashicorp/tls + + Note over User,TLS: terraform apply + + User->>TF: create Folder + Project + TF-->>User: project_id + + User->>TF: create Network + Security Group + TF-->>User: network_id + + User->>TLS: generate RSA Private Key + TLS-->>User: private_key_pem + + User->>TLS: sign Self-Signed Certificate (private_key_pem) + TLS-->>User: cert_pem + + User->>TF: upload ALB Certificate (cert_pem + private_key_pem) + TF-->>User: cert_id + + User->>TF: create VM (cloud-init → Docker · nginx) + TF-->>User: private_ip · vm_public_ip + + User->>TF: create Public IP + DNS Zone + A-Record + TF-->>User: alb_public_ip · zone ready + + User->>TF: create ALB (cert_id · private_ip · alb_public_ip) + TF-->>User: ✅ https://your-domain ready +``` + +--- + +# VM + ALB + Self-Signed TLS + +## Traffic Flow + +``` + Client + │ + │ DNS lookup: vm-alb-tls.stackit.gg + ▼ + STACKIT DNS + │ resolves to ALB public IP (Terraform A-record) + │ + │ HTTPS :443 + ▼ + STACKIT ALB (L7, TLS termination) + │ certificate: self-signed (hashicorp/tls, managed by Terraform) + │ HTTP routing to target pool + │ + │ HTTP :80 + ▼ + STACKIT VM (Debian 12, Docker Engine) + │ + ▼ + Container: nginx:alpine (port 80) +``` + +The ALB terminates TLS. The VM only receives plain HTTP on port 80 — no +certificate management on the backend. + +--- + +## Component Responsibility + +| Component | File | Purpose | +| ------------------------- | -------------------------- | --------------------------------- | +| STACKIT Folder + Project | `02-resource-hierarchy.tf` | Resource boundary | +| Network + Security Group | `03-network.tf` | Private network, SSH + HTTP rules | +| VM (Debian 12) | `04-compute.tf` | Docker host | +| TLS Private Key | `05-certificate.tf` | RSA key pair (hashicorp/tls) | +| Self-Signed Certificate | `05-certificate.tf` | X.509 cert (hashicorp/tls) | +| ALB Certificate resource | `05-certificate.tf` | Uploads cert to STACKIT | +| DNS Zone + A-record | `05-dns.tf` | Zone apex → ALB public IP | +| Application Load Balancer | `06-alb.tf` | L7 TLS termination + HTTP routing | + +--- + +## Certificate Flow + +``` + hashicorp/tls provider STACKIT + ───────────────────── ─────── + tls_private_key ─ PEM ─► stackit_alb_certificate ─ cert_id ─► stackit_application_load_balancer + tls_self_signed_cert ─ PEM ─► (certificate store) (HTTPS listener) +``` + +All steps happen in a single `terraform apply`. No scripts or manual API calls required. + +--- + +## Resource Hierarchy + +``` +STACKIT Organisation +└── Folder: alb-showcase (imported via terraform import) + └── Project: vm-alb-self-signed-cert (created by Terraform) + ├── Network: vm-alb-tls-net (10.10.0.0/24, routed) + │ └── Security Group: vm-alb-tls-vm-sg + │ ├── Ingress TCP 22 ← admin_cidr only + │ ├── Ingress TCP 80 ← 0.0.0.0/0 (ALB backend traffic) + │ └── Egress all → 0.0.0.0/0 + ├── VM: vm-alb-tls-vm (Debian 12, Docker Engine) + ├── Certificate: vm-alb-tls-selfsigned-cert (self-signed) + ├── DNS Zone: vm-alb-tls.stackit.gg (primary) + └── ALB: vm-alb-tls-alb + ├── Public IP (dedicated, wired to DNS A-record by Terraform) + ├── Listener HTTP :80 → VM:80 + └── Listener HTTPS :443 → VM:80 (TLS terminated at ALB) +``` + +--- + +## Security Notes + +| Concern | Status | +| ------------------------------ | ----------------------------------------------------------- | +| Private key in Terraform state | Yes — acceptable for showcase/dev | +| Certificate trust | Self-signed — browsers will warn; use `curl -k` for testing | +| SSH access | Restricted to `admin_cidr` — never use `0.0.0.0/0` | +| Production recommendation | Use `stackit-acme-alb` for Let's Encrypt certs | diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/backend.conf.example b/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/backend.conf.example new file mode 100644 index 0000000..c19f34e --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/backend.conf.example @@ -0,0 +1,15 @@ +bucket = "" +key = "" +region = "eu01" + +endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" +} + +access_key = "" +secret_key = "" + +skip_credentials_validation = true +skip_region_validation = true +skip_requesting_account_id = true +skip_s3_checksum = true diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/terraform.tfvars.example b/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/terraform.tfvars.example new file mode 100644 index 0000000..d2ad220 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/terraform.tfvars.example @@ -0,0 +1,52 @@ +# Copy this file to terraform.tfvars and fill in the required values. +# terraform.tfvars is gitignored — never commit real credentials or IDs. + +# ─── Provider ───────────────────────────────────────────────────────────────── + +stackit_region = "eu01" +stackit_service_account_key_path = "keys/sa-key.json" + +# ─── Project ────────────────────────────────────────────────────────────────── + +# Find your project ID in the STACKIT Portal or via: stackit project list +project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +# ─── Naming ─────────────────────────────────────────────────────────────────── + +name_prefix = "vm-alb-tls" + +# ─── Network ────────────────────────────────────────────────────────────────── + +network_cidr = "10.10.0.0/24" + +# Restrict SSH to your IP — never use 0.0.0.0/0 in production +# Find your IP: curl -s https://ifconfig.schwarz +admin_cidr = "203.0.113.10/32" + +# ─── Compute / VM ───────────────────────────────────────────────────────────── + +machine_type = "g1.1" +availability_zone = "eu01-1" + +# List available Debian 12 images: +# stackit image list --all --project-id +image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +boot_volume_size_gb = 20 +keypair_name = "vm-alb-tls-key" + +# Paste your SSH public key here (never the private key) +ssh_public_key = "ssh-ed25519 AAAA... your-key-comment" + +# ─── TLS / Certificate ──────────────────────────────────────────────────────── + +# Use the ALB public IP or a hostname you control. +# A self-signed cert on an IP address is fully valid for testing. +tls_common_name = "alb.example.internal" +tls_organization = "STACKIT ALB Showcase" +tls_validity_hours = 8760 # 1 year + +# ─── ALB ────────────────────────────────────────────────────────────────────── + +alb_plan_id = "p10" +alb_acl_ranges = ["0.0.0.0/0"] diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/templates/cloud-init.yaml.tpl b/examples/alb-tls-examples/vm-alb-self-signed-cert/templates/cloud-init.yaml.tpl new file mode 100644 index 0000000..d0a5b0c --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/templates/cloud-init.yaml.tpl @@ -0,0 +1,37 @@ +#cloud-config + +package_update: true +package_upgrade: false +packages: + - ca-certificates + - curl + - gnupg + +write_files: + - path: /opt/install-docker.sh + permissions: "0755" + content: | + #!/bin/bash + set -euo pipefail + + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc + chmod a+r /etc/apt/keyrings/docker.asc + + . /etc/os-release + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ + https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" \ + > /etc/apt/sources.list.d/docker.list + + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + docker-ce docker-ce-cli containerd.io docker-compose-plugin + + systemctl enable --now docker + usermod -aG docker debian + docker run -d --name nginx --restart unless-stopped -p 80:80 nginx:alpine + +runcmd: + - /opt/install-docker.sh + +final_message: "Cloud-init complete after $UPTIME seconds." diff --git a/examples/iaas-cross-az-layer4-loadbalancer/apache-debug-user.yaml b/examples/iaas-cross-az-layer4-loadbalancer/apache-debug-user.yaml index c44cda9..74c4065 100644 --- a/examples/iaas-cross-az-layer4-loadbalancer/apache-debug-user.yaml +++ b/examples/iaas-cross-az-layer4-loadbalancer/apache-debug-user.yaml @@ -1,3 +1,17 @@ +# 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. + #cloud-config users: - name: debug diff --git a/examples/iaas-cross-az-layer7-loadbalancer-waf/apache-debug-user.yaml b/examples/iaas-cross-az-layer7-loadbalancer-waf/apache-debug-user.yaml index c44cda9..74c4065 100644 --- a/examples/iaas-cross-az-layer7-loadbalancer-waf/apache-debug-user.yaml +++ b/examples/iaas-cross-az-layer7-loadbalancer-waf/apache-debug-user.yaml @@ -1,3 +1,17 @@ +# 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. + #cloud-config users: - name: debug diff --git a/examples/iaas-ha-vrrp/cloud-init.yaml b/examples/iaas-ha-vrrp/cloud-init.yaml index 596b229..8224582 100644 --- a/examples/iaas-ha-vrrp/cloud-init.yaml +++ b/examples/iaas-ha-vrrp/cloud-init.yaml @@ -1,3 +1,17 @@ +# 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. + #cloud-config # Update apt database on first boot (run 'apt-get update'). # Note, if packages are given, or package_upgrade is true, then diff --git a/examples/iaas-volume-encryption/cloud-init.yaml b/examples/iaas-volume-encryption/cloud-init.yaml index 653b5c6..8b26cf3 100644 --- a/examples/iaas-volume-encryption/cloud-init.yaml +++ b/examples/iaas-volume-encryption/cloud-init.yaml @@ -1,3 +1,17 @@ +# 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. + #cloud-config users: - name: Administrator diff --git a/examples/opnsense-hub-and-spoke/cloud-init/user-init-linux.yml b/examples/opnsense-hub-and-spoke/cloud-init/user-init-linux.yml index be6e579..fb07b83 100644 --- a/examples/opnsense-hub-and-spoke/cloud-init/user-init-linux.yml +++ b/examples/opnsense-hub-and-spoke/cloud-init/user-init-linux.yml @@ -1,3 +1,17 @@ +# 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. + #cloud-config # --------------------------------------------------------------------------- # Example cloud-init for Linux instances (RHEL / Debian). diff --git a/examples/opnsense-hub-and-spoke/cloud-init/user-init-windows.yml b/examples/opnsense-hub-and-spoke/cloud-init/user-init-windows.yml index 89103d4..e7bf80d 100644 --- a/examples/opnsense-hub-and-spoke/cloud-init/user-init-windows.yml +++ b/examples/opnsense-hub-and-spoke/cloud-init/user-init-windows.yml @@ -1,3 +1,17 @@ +# 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. + #cloud-config # --------------------------------------------------------------------------- # Example cloud-init for Windows Server instances. diff --git a/examples/ske-stackit-sfs-integration/PersistentVolumeClaim.yaml b/examples/ske-stackit-sfs-integration/PersistentVolumeClaim.yaml index 9247fb1..28104d8 100644 --- a/examples/ske-stackit-sfs-integration/PersistentVolumeClaim.yaml +++ b/examples/ske-stackit-sfs-integration/PersistentVolumeClaim.yaml @@ -1,3 +1,17 @@ +# 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. + kind: PersistentVolumeClaim apiVersion: v1 metadata: diff --git a/examples/ske-stackit-sfs-integration/example-rwx-deployment.yaml b/examples/ske-stackit-sfs-integration/example-rwx-deployment.yaml index 08fd14d..9163dd1 100644 --- a/examples/ske-stackit-sfs-integration/example-rwx-deployment.yaml +++ b/examples/ske-stackit-sfs-integration/example-rwx-deployment.yaml @@ -1,3 +1,17 @@ +# 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. + apiVersion: v1 kind: Service metadata: diff --git a/examples/vpn-usecases/module/stackit-sna-with-debug-machine/debug-user.yml b/examples/vpn-usecases/module/stackit-sna-with-debug-machine/debug-user.yml index 865ffb5..90b0227 100644 --- a/examples/vpn-usecases/module/stackit-sna-with-debug-machine/debug-user.yml +++ b/examples/vpn-usecases/module/stackit-sna-with-debug-machine/debug-user.yml @@ -1,3 +1,17 @@ +# 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. + #cloud-config # --------------------------------------------------------------------------- # Example cloud-init for Linux instances (RHEL / Debian).