diff --git a/AGENTS.de.md b/AGENTS.de.md new file mode 100644 index 0000000..2600a8d --- /dev/null +++ b/AGENTS.de.md @@ -0,0 +1,54 @@ + + +# Repository-Leitlinien + +[English](AGENTS.md) | [Deutsch](AGENTS.de.md) + +Vor Codeaenderungen [CONTRIBUTING.de.md](CONTRIBUTING.de.md) lesen. +Fuer Terraform auch den [Terraform-Stilguide](docs/terraform-style.de.md) +lesen und befolgen; die dokumentierten Konventionen fuer +Root-/Child-Provider-Versionen erhalten. + + + +## Grenzen + +- `src/` ist der unabhaengig nutzbare Accelerator. + Keine Abhaengigkeit von Configurator-API, Datenbank oder gehostetem Service + einfuehren. +- `landing-zone-configurator/` enthaelt eigene App, Vertraege, Compiler, + HCL-Adapter, Runner-Paketierung und Betreiber-IaC. + Der [komponentenuebergreifenden Entwicklungsuebersicht](landing-zone-configurator/docs/development.de.md) + folgen. +- Native Eingaben, generierte Kataloge, Domaenenvalidierung, API-/Jobvertraege, + Frontend/Sprachen, Runner und Assistant-Wissen konsistent halten. + Artefakte mit bestehenden Befehlen generieren; + generierte Inhalte nicht manuell bearbeiten. +- Importierte Felder, stabile Ressourcenidentitaeten, Null-/Default-Semantik, + Tenant-Autorisierung und revisionsgebundene Freigaben erhalten. + Assistant-Annahme ist weder Persistenz noch Erlaubnis fuer Plan/Apply. + + + +## Verifikation und Betrieb + +- Gepinnte Tools und kleinste relevante bestehende Pruefungen verwenden, + danach den vollstaendigen betroffenen Ablauf validieren. + Uebersprungene Pruefungen und offene Fehler explizit berichten. +- Aenderungen an Assistant-Fakten oder -Faehigkeiten benoetigen + quellengebundene Realmodell-Qualifikation gemaess Wissensvertrag. + Fehlgeschlagene Wiederholungen sind Fehler, keine Retries; + Validierung nicht fuer einen gruenen Nachweis abschwaechen. +- Niemals Secrets, Kundenzustand, rohe Plaene oder private Betriebsevidenz + offenlegen. + Keine Cloud-Mutationen oder Deployments ohne explizite Autorisierung. +- Configurator-Provisionierung und Anwendungsdeployment sind manuelle + Workflows. + Ein Source-Merge ist keine Deployment-Freigabe; + Produktion ist ein getrennter Folgeschritt. +- Nicht betroffene Worktree-Aenderungen erhalten. + Direkt zugehoerige oeffentliche und Contributor-Dokumentation + aktualisieren, keine datierten Sitzungsnotizen oder Uebergabedateien. +- Englisch als primaere Dokumentationssprache und deutsche Begleiter + gemaess [CONTRIBUTING.de.md](CONTRIBUTING.de.md) pflegen; + generierte und maschinell eingelesene Quellvertraege erhalten. diff --git a/AGENTS.md b/AGENTS.md index 5f9dbaf..57e7a04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ # Repository guidance +[English](AGENTS.md) | [Deutsch](AGENTS.de.md) + Read [CONTRIBUTING.md](CONTRIBUTING.md) before changing code. For Terraform, also read and follow [the Terraform style guide](docs/terraform-style.md); preserve the documented root/child provider-version conventions. @@ -31,3 +33,6 @@ preserve the documented root/child provider-version conventions. A source merge is not deployment approval; production is a separate follow-up. - Preserve unrelated worktree changes. Update directly related public and contributor documentation, not dated session notes or handoff files. +- Maintain English-first documentation and German companions according to + [CONTRIBUTING.md](CONTRIBUTING.md); preserve generated and machine-ingested + source contracts. diff --git a/CONTRIBUTING.de.md b/CONTRIBUTING.de.md new file mode 100644 index 0000000..9ca09b6 --- /dev/null +++ b/CONTRIBUTING.de.md @@ -0,0 +1,161 @@ + +# Mitwirken + +[English](CONTRIBUTING.md) | [Deutsch](CONTRIBUTING.de.md) + +Vielen Dank, dass Sie sich die Zeit nehmen, einen Beitrag zu leisten. Dieses Dokument beschreibt die lokale Einrichtung, die Konventionen dieses Repositorys und den erwarteten Inhalt eines Pull Requests. + + +## Lokale Einrichtung + +Die Werkzeugversionen sind in `mise.toml` festgelegt. Installieren Sie sie einmalig: + +```bash +mise install +``` + +Damit stehen OpenTofu, die STACKIT CLI, `terraform-docs` und `pre-commit` in den Versionen bereit, gegen die dieses Repository getestet wird. + +Installieren Sie die Git-Hooks: + +```bash +pre-commit install +``` + +Die Hooks führen `tofu fmt` aus und erzeugen die Moduldokumentation neu. Wenn ein Hook eine Datei umschreibt, nehmen Sie das Ergebnis in den Staging-Bereich auf und versuchen Sie den Commit erneut. + +Der Dokumentations-Hook ruft `terraform-docs` über `mise exec` auf. So verwendet er die in `mise.toml` festgelegte Version und funktioniert auch, wenn Git aus einem Editor ohne geladene mise-Umgebung gestartet wird. + +Für eine Bereitstellung in einer echten STACKIT-Organisation ist zusätzlich ein Dienstkontoschlüssel erforderlich. `mise.toml` erwartet ihn unter `~/.stackit/credentials.json`; siehe [docs/getting-started.md](docs/getting-started.de.md). + + +## Vor dem Öffnen eines Pull Requests + +```bash +cd src +tofu fmt -recursive +tofu init -backend=false +tofu validate +tofu test +``` + +`tofu test` führt die Konfigurationssuiten in `src/tests/` als lokale, reine +Plan-Läufe mit Mock-Providern aus. Diese Tests erstellen keine Cloud-Ressourcen. +Die Prüfung in der echten Cloud ist ein separater, manuell gestarteter Workflow, +keine gewöhnliche PR-Prüfung. + + +## Konventionen + + +### Änderungen über Accelerator und Configurator hinweg + +Der Accelerator bleibt unabhängig nutzbar. Configurator-Funktionen betreffen oft +mehrere Komponenten; aktualisieren Sie den vollständigen Vertrag, nicht nur ein +sichtbares UI-Feld. Lesen Sie zuerst die +[Entwicklungsübersicht](landing-zone-configurator/docs/development.de.md) und die +[Architektur](landing-zone-configurator/docs/architecture.de.md). + +1. **Native Infrastruktur:** Ergänzen oder ändern Sie Root-/Moduleingaben und -ausgaben, + Standardwerte, Validierung und Modultests in `src/`. Bewahren Sie State-Adressen und + Kompatibilität oder dokumentieren Sie die Migration ausdrücklich. +2. **Eingabe-/Katalogadapter:** Aktualisieren Sie HCL-Import/-Export und erzeugen Sie + die geprüften Kataloge für native Eingaben und Templates über die bestehenden + [Adapterbefehle](landing-zone-configurator/tools/hcl-adapter/README.de.md) neu. +3. **Domain/Compiler:** Verdrahten Sie native Felder, die Semantik von optional/null/Standardwerten, + die Erhaltung von Werten und deterministische Validierung in `app/packages/domain`. +4. **API/Verträge:** Aktualisieren Sie gemeinsame Anfrage-/Jobverträge und serverseitige Mandanten-, + Berechtigungs-, Revisions- und Lebenszyklusprüfungen. Verlassen Sie sich nicht auf reine Browserprüfungen. +5. **Frontend:** Bilden Sie denselben Vertrag, beide Sprachen, hilfreiche Validierung und + ausdrückliche Prüfung/Annahme ab. Testen Sie Desktop/Mobilgeräte und den Erhalt von Entwürfen. +6. **Runner/Release:** Führen Sie geänderte Eingaben und Ausgaben durch Broker, State, + Migrationen, festgelegte Pakete und Jobergebnisse. Halten Sie die Quellherkunft von API/UI + und Runner konsistent; führen Sie niemals einen beliebigen Repository-Head aus. +7. **Assistant:** Prüfen Sie Themenzuständigkeit, kuratierte Fakten, Berechtigungen und Vorschlagsschemata. + Erzeugen Sie Wissen/Abdeckung neu, ergänzen Sie faktische positive und negative + Oracle-Tests und qualifizieren Sie betroffene Fälle mit dem echten Modell mindestens zweimal. + Eine neue Fähigkeit oder eine breite Vertragsänderung benötigt außerdem die vollständige + wiederholte Suite und den durchgängigen Dialog. Schwächen Sie kein Oracle ab, um einen echten Fehler zu verbergen. +8. **Dokumentation:** Aktualisieren Sie sichtbares Benutzerverhalten, Architektur und Hinweise + für Mitwirkende. Verlinken Sie Folge-Issues für bewusst gesetzte Grenzen. + +Verwenden Sie für die Configurator-Einrichtung Node **24.21.0**, npm **11.19.0** und dessen eigene Lockdatei: + +```sh +cd landing-zone-configurator/app +npm ci +npm run check +``` + +Verwenden Sie während der Entwicklung die kleinsten relevanten Tests und vor der +Prüfung die zutreffenden Integrationsprüfungen: + +| Änderung | Erforderlicher Nachweis | +| --- | --- | +| Native Eingaben/Templates | Modultests; Adapter `go test ./...` und Katalogprüfungen; `npm run test:common-contract` | +| Domain/API/UI | `npm run check`; relevante Datenbank- und Playwright-Tests | +| Broker, Vorbereitungen, Runner oder State | Relevante Worker-/Brokertests; `npm run test:platform-database`; Paketierungs- und State-Regressionstests | +| Assistant | `npm run assistant:generate`, Formatierung generierter Dateien, `npm run assistant:check`, faktische Unit-Tests und quellgebundene Live-Nachweise | +| Operator-CI/IaC | Relevante `node --test`-Prüfungen; Workflow-Lint; Formatierung/Initialisierung/Validierung und Mock-Tests für betroffene IaC-Roots | + +Die [Anwendungs-README](landing-zone-configurator/app/README.de.md) dokumentiert die +genauen Befehle für Generierung, Datenbank, Browser und das echte Modell. +Datenbank-/Browsertests verwenden isolierte Testdienste. Gewöhnliche PR-Prüfungen +dürfen keine Kundeninfrastruktur bereitstellen. Teilen Sie keine +Ausführungszugangsdaten mit der ausschließlich für Inferenz vorgesehenen Umgebung +`assistant-tests`. Configurator-Provisionierung und -Bereitstellung bleiben manuell; +die Produktionseinrichtung ist eine separate Aufgabe. + +Der [Workflow-Katalog](docs/workflows.de.md) führt alle Pipelines, automatischen/manuellen +Auslöser und Startanleitungen für die Oberfläche auf. Aktualisieren Sie ihn bei Änderungen +an Workflow-Namen, Auslösern, Eingaben oder betrieblichen Freigabeschranken; halten Sie +die Bereitstellungsfreigabe getrennt von der PR-Freigabe. + + +### Terraform-Konventionen + +Befolgen Sie neben den folgenden bestehenden Modulkonventionen den vollständigen +[Terraform-Stilleitfaden](docs/terraform-style.de.md). + +**Terraform-Struktur.** Jedes Modul hält seinen `terraform`-Block in `terraform.tf`, seine Variablen in `variables.tf` und seine Ausgaben in `outputs.tf`. Ressourcendateien erhalten ein numerisches Präfix, wenn die Lesereihenfolge wichtig ist; jede Ressourcengruppe wird durch einen eingerahmten Abschnittskopf eingeleitet. + +**Provider-Versionen.** Das Root-Modul in `src/terraform.tf` legt jeden Provider auf eine exakte Version fest. Child-Module unter `src/modules/` deklarieren nur eine Mindestversion mit `>=`. Fügen Sie niemals eine Obergrenze in einem Child-Modul hinzu; diese blockiert unbemerkt ein Upgrade im Root-Modul. + +**Benennung.** `snake_case` für alle Blockbezeichnungen, Variablen, Ausgaben und lokalen Werte. Ressourcennamen wiederholen den Ressourcentyp nicht. + +**Variablen und Ausgaben.** Jede Variable benötigt `type` und `description`. Jede Ausgabe benötigt `description` vor `value`. + +**Kommentare.** Ein Kommentar erklärt eine Einschränkung, die der Code nicht zeigen kann, etwa einen Provider-Fehler oder eine Besonderheit der OPNsense-API. Beschreibungen der Funktionsweise des Codes oder des Architekturzusammenhangs gehören in [docs/architecture.md](docs/architecture.de.md). + +**Dokumentation.** Modul-READMEs werden generiert. Bearbeiten Sie den Block zwischen den Markern `BEGIN_TF_DOCS` und `END_TF_DOCS` nicht von Hand; führen Sie stattdessen den pre-commit-Hook aus. + +**Beispielkonfiguration.** Die tfvars-Dateien in `src/config/` dienen als Dokumentation. Belassen Sie Platzhalterwerte wie `owner@example.com` und eine Null-UUID für die Organisation. Committen Sie niemals eine Kennung aus einer echten Umgebung. + + +### Zweisprachige Dokumentation + +Englische `.md`-Dateien sind maßgeblich; Dokumentation für Menschen erhält +vollständige deutsche `.de.md`-Pendants, keine Zusammenfassungen. Setzen Sie die +wechselseitige Navigation `[English](name.md) | [Deutsch](name.de.md)` unmittelbar +nach jede H1 und aktualisieren Sie bei Prosaänderungen beide Sprachen. Bewahren Sie +Befehle, Beispiele, exakte Werte und bestehende Überschriftenanker. + +Lassen Sie generatorverwaltete Verträge unverändert: Erzeugen Sie die englische +Referenz mit den bestehenden Werkzeugen neu und übernehmen Sie ihre technischen +Blöcke wortgetreu in das deutsche Pendant, mit einer deutschen Einleitung samt +Link zur maßgeblichen Referenz. Maschinell eingelesene Assistant-Themen bleiben +unverändert auf Englisch; verzeichnen Sie ihre separaten deutschen Übersetzungen +für Menschen in der Assistant-Wissens-README, statt die eingelesenen Themen zu ändern. + + +## Commits und Pull Requests + +Commit-Nachrichten folgen [Conventional Commits](https://www.conventionalcommits.org/), mit dem Modul als optionalem Scope: + +``` +feat(connectivity): add active/passive CARP support +fix(audit-logs): add HTTPS scheme to URLs +docs: clarify firewall bootstrap steps +``` + +Öffnen Sie den Pull Request gegen `main`. Beschreiben Sie, was sich für Nutzer der Landing Zone ändert, und geben Sie an, mit welcher Variante Sie getestet haben. CI führt Formatierung, Validierung, TFLint und die Testsuiten aus; alle müssen vor der Prüfung erfolgreich sein. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3cbd85f..3aaa96e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,7 @@ # Contributing +[English](CONTRIBUTING.md) | [Deutsch](CONTRIBUTING.de.md) + Thanks for taking the time to contribute. This document describes the local setup, the conventions this repository follows, and what a pull request is expected to contain. ## Local setup @@ -120,6 +122,20 @@ existing module conventions below. **Example configuration.** The tfvars files in `src/config/` are documentation. Keep them on placeholder values such as `owner@example.com` and a zero UUID for the organization. Never commit an identifier from a real environment. +### Bilingual documentation + +English `.md` files are canonical; human-facing documentation has complete German +`.de.md` peers, not summaries. Put reciprocal `[English](name.md) | [Deutsch](name.de.md)` +navigation immediately after each H1 and update both languages whenever prose +changes. Preserve commands, examples, exact values and existing heading anchors. + +Keep generator-owned contracts unchanged: regenerate the English reference with +the existing tooling and copy its technical blocks verbatim into the German peer, +with a German introduction linking the canonical reference. Machine-ingested +Assistant topics remain unchanged in English; index their separate human-readable +German translations in the Assistant knowledge README rather than changing the +ingested topics. + ## Commits and pull requests Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/), with the module as the optional scope: diff --git a/README.de.md b/README.de.md new file mode 100644 index 0000000..d298f21 --- /dev/null +++ b/README.de.md @@ -0,0 +1,45 @@ +
object({
retention_days = optional(number, 180)
acl = optional(list(string), ["0.0.0.0/0"])
s3_object_lock = optional(bool, true)
link_scopes = optional(list(object({
resource_type = string # organization, folder, project
resource_id = string
})), null)
}) | `null` | no |
+| [company\_code](#input\_company\_code) | Company code used in resource naming conventions. | `string` | n/a | yes |
+| [company\_name](#input\_company\_name) | Name of the company. | `string` | n/a | yes |
+| [connectivity](#input\_connectivity) | Connectivity configuration including DNS zones, network areas, firewalls, and VPN. Use network\_areas with descriptive keys that reflect any business, security, tenant, or regional boundary; network\_area and firewall remain supported for existing single-area configurations. Use firewalls for per-area appliance settings. | object({
naming_pattern = optional(string, null)
dns_zones = optional(map(object({
dns_name = string
network_area_key = optional(string, "default")
region = optional(string, null)
name = optional(string, null)
contact_email = optional(string, null)
type = optional(string, "primary")
acl = optional(string, null)
description = optional(string, null)
default_ttl = optional(number, 3600)
})), {})
network_area = optional(object({
ranges = list(string)
transfer_network = string
min_prefix_length = optional(number, 24)
max_prefix_length = optional(number, 28)
default_prefix_length = optional(number, 28)
# Unset means the STACKIT resolvers of var.region, see modules/connectivity/1-network-area.tf.
default_nameservers = optional(list(string), null)
}), null)
network_areas = optional(map(object({
name = optional(string, null)
ranges = list(string)
transfer_network = string
min_prefix_length = optional(number, 24)
max_prefix_length = optional(number, 28)
default_prefix_length = optional(number, 28)
default_nameservers = optional(list(string), null)
})), null)
firewall = optional(object({
zone = string
flavor = string
name = string
volume_performance_class = optional(string, "storage_premium_perf4")
volume_size = optional(number, 16)
lan_network_range = string
wan_network_range = string
lan_ip = optional(string, null)
wan_ip = optional(string, null)
# Active/passive CARP pair. Adds a second appliance in backup_zone and a CARP
# virtual IP on the LAN that replaces the primary's LAN IP as the next hop the
# landing zone routes point at, so a node failure never touches the routes.
# See docs/architecture.md#high-availability-optional.
ha = optional(object({
backup_zone = string
backup_name = optional(string, null) # defaults to "-backup"
backup_lan_ip = optional(string, null) # defaults to the 6th address of lan_network_range
backup_wan_ip = optional(string, null) # defaults to the 6th address of wan_network_range
lan_vip = optional(string, null) # defaults to the 7th address of lan_network_range
vhid = optional(number, 1)
}), null)
}), null)
firewalls = optional(map(object({
zone = string
flavor = string
name = string
volume_performance_class = optional(string, "storage_premium_perf4")
volume_size = optional(number, 16)
lan_network_range = string
wan_network_range = string
lan_ip = optional(string, null)
wan_ip = optional(string, null)
ha = optional(object({
backup_zone = string
backup_name = optional(string, null)
backup_lan_ip = optional(string, null)
backup_wan_ip = optional(string, null)
lan_vip = optional(string, null)
vhid = optional(number, 1)
}), null)
})), null)
vpn = optional(object({
display_name = optional(string, null)
plan_id = optional(string, "p100")
routing_type = optional(string, "ROUTE_BASED")
availability_zones = object({
tunnel1 = string
tunnel2 = string
})
connections = optional(map(object({
display_name = optional(string, null)
enabled = optional(bool, true)
local_subnets = optional(list(string), null)
remote_subnets = optional(list(string), null)
static_routes = optional(list(string), null)
tunnel1 = object({
remote_address = string
peering = optional(object({
local_address = string
remote_address = string
}), null)
phase1 = optional(object({
encryption_algorithms = optional(list(string), ["aes256"])
integrity_algorithms = optional(list(string), ["sha2_384"])
dh_groups = optional(list(string), ["ecp384"])
rekey_time = optional(number, null)
}), {})
phase2 = optional(object({
encryption_algorithms = optional(list(string), ["aes256"])
integrity_algorithms = optional(list(string), ["sha2_384"])
dh_groups = optional(list(string), ["ecp384"])
rekey_time = optional(number, null)
dpd_action = optional(string, null)
start_action = optional(string, null)
}), {})
})
tunnel2 = object({
remote_address = string
peering = optional(object({
local_address = string
remote_address = string
}), null)
phase1 = optional(object({
encryption_algorithms = optional(list(string), ["aes256"])
integrity_algorithms = optional(list(string), ["sha2_384"])
dh_groups = optional(list(string), ["ecp384"])
rekey_time = optional(number, null)
}), {})
phase2 = optional(object({
encryption_algorithms = optional(list(string), ["aes256"])
integrity_algorithms = optional(list(string), ["sha2_384"])
dh_groups = optional(list(string), ["ecp384"])
rekey_time = optional(number, null)
dpd_action = optional(string, null)
start_action = optional(string, null)
}), {})
})
})), {})
}), null)
}) | `null` | no |
+| [connectivity\_regions](#input\_connectivity\_regions) | Regional connectivity configuration for the statically supported eu01 and eu02 hubs. Set to null to use the single-region connectivity input. | `map(any)` | `null` | no |
+| [devops](#input\_devops) | DevOps module configuration. Set to null to skip deployment. | object({
git_flavor = optional(string, null)
allowed_network_ranges = optional(list(string), ["0.0.0.0/0"])
}) | `null` | no |
+| [federated\_identity\_providers](#input\_federated\_identity\_providers) | List of federated identity providers to configure for the management service account. | list(object({
name = string
issuer = string
assertions = list(object({
item = string
operator = string
value = string
}))
})) | `[]` | no |
+| [firewall\_admin\_password](#input\_firewall\_admin\_password) | Password of firewall\_admin\_username, used once to derive the API key. Defaults to the password baked into the STACKIT OPNsense image. Override through TF\_VAR\_firewall\_admin\_password after rotating it, or set to null and supply firewall\_api\_credentials instead. | `string` | `"STACKIT123!"` | no |
+| [firewall\_admin\_username](#input\_firewall\_admin\_username) | Login used to derive the API key from the appliance. The STACKIT OPNsense image ships with root. | `string` | `"root"` | no |
+| [firewall\_api\_credentials](#input\_firewall\_api\_credentials) | OPNsense API key and secret. Bootstrap them once from the appliance login with src/modules/firewall-config/scripts/bootstrap-api-key.sh, then supply through TF\_VAR\_firewall\_api\_credentials or a gitignored tfvars file. Kept out of firewall\_config so the policy stays committable. | object({
api_key = string
api_secret = string
}) | `null` | no |
+| [firewall\_api\_secret\_version](#input\_firewall\_api\_secret\_version) | Write version of the OPNsense API key in the Secrets Manager. The key is only transmitted when this number changes, so bump it by one after a re-bootstrap has produced a fresh key, otherwise the Secrets Manager keeps serving the old one. | `number` | `1` | no |
+| [firewall\_bootstrap](#input\_firewall\_bootstrap) | Pass -var firewall\_bootstrap=true on the two greenfield applies that create the OPNsense API key. It suppresses the Secrets Manager read, which would otherwise fail at plan time because the secret does not exist yet. Never set it on an established deployment: without the bootstrap cache file it plans the destruction of the policy and the stored key. The steady state needs no variable at all. | `bool` | `false` | no |
+| [firewall\_config](#input\_firewall\_config) | Policy pushed to one OPNsense appliance through its API. Requires connectivity.firewall to be deployed and firewall\_api\_credentials to be set. Set to null to leave the appliance untouched. | object({
# Defaults to https://, derived from connectivity.firewall.
endpoint = optional(string, null)
insecure = optional(bool, true)
aliases = optional(map(object({
type = optional(string, "network")
enabled = optional(bool, true)
description = optional(string, null)
content = optional(list(string), [])
update_freq = optional(number, null)
stats = optional(bool, false)
})), {})
routes = optional(map(object({
enabled = optional(bool, true)
network = string
gateway = string
description = optional(string, null)
})), {})
rules = optional(map(object({
sequence = optional(number, 100)
enabled = optional(bool, true)
action = optional(string, "pass")
direction = optional(string, "in")
interfaces = optional(list(string), ["lan"])
protocol = optional(string, "any")
ip_protocol = optional(string, "inet")
quick = optional(bool, true)
source_net = optional(string, "any")
source_port = optional(string, null)
source_invert = optional(bool, false)
destination_net = optional(string, "any")
destination_port = optional(string, null)
destination_invert = optional(bool, false)
log = optional(bool, false)
description = optional(string, null)
})), {})
outbound_nat = optional(map(object({
sequence = optional(number, 200)
enabled = optional(bool, true)
interface = optional(string, "wan")
protocol = optional(string, "any")
ip_protocol = optional(string, "inet")
source_net = optional(string, "any")
destination_net = optional(string, "any")
target_ip = optional(string, "wanip")
disable_nat = optional(bool, false)
log = optional(bool, false)
description = optional(string, null)
})), {})
port_forwards = optional(map(object({
sequence = optional(number, 100)
enabled = optional(bool, true)
interfaces = optional(list(string), ["wan"])
protocol = optional(string, "TCP")
ip_protocol = optional(string, "inet")
source_net = optional(string, "any")
destination_net = optional(string, "wanip")
destination_port = string
target_ip = string
target_port = optional(string, null)
nat_reflection = optional(string, "default")
log = optional(bool, true)
description = optional(string, null)
})), {})
}) | `null` | no |
+| [labels](#input\_labels) | Additional labels to apply to all resources. | `map(string)` | `{}` | no |
+| [landing\_zone\_namespace\_services](#input\_landing\_zone\_namespace\_services) | Map of namespace-service configurations per landing zone key. If a key is present, namespace service is enabled for that landing zone. | map(object({
demo_enabled = optional(bool, false)
demo_metrics_ingestion = optional(object({
enabled = optional(bool, false)
target_urls = optional(list(string), [])
scheme = optional(string, "https")
metrics_path = optional(string, "/")
scrape_interval = optional(string, "70s")
scrape_timeout = optional(string, "30s")
}), {})
namespace = optional(string, null)
dns_subdomain = optional(string, null)
secretsmanager = optional(bool, true)
sample_load = optional(object({
enabled = optional(bool, false)
image = optional(string, "busybox:1.36")
}), {})
secrets_enforcement = optional(object({
enabled = optional(bool, false)
mode = optional(string, "audit")
allow_opaque_secret_types = optional(list(string), [])
break_glass = optional(object({
enabled = optional(bool, true)
ttl_hours = optional(number, 24)
principals = optional(list(string), [])
}), {})
}), {})
kubernetes_access = optional(object({
enabled = optional(bool, true)
service_account_name = optional(string, null)
}), {})
labels = optional(map(string), {})
annotations = optional(map(string), {})
})) | `{}` | no |
+| [landing\_zones](#input\_landing\_zones) | Map of landing zones to create. Public landing zones can create a local network with network\_enabled. Corporate landing zones use network\_area\_key to select a connectivity.network\_areas entry; set region to eu01 or eu02 with connectivity\_regions. | map(object({
project_name = string
project_code = string
owner_email = string
# Set to true for corporate landing zones (connected to network area), false for public
corporate = optional(bool, true)
network_area_key = optional(string, "default")
region = optional(string, null)
env = optional(string, "dev")
role_assignments = optional(list(object({
role = string
subject = string
})), [])
network_enabled = optional(bool, false)
network_prefix_length = optional(number, null)
secretsmanager_enabled = optional(bool, true)
custom_roles = optional(list(object({
name = string
description = string
permissions = list(string)
})), [])
observability = optional(object({
enabled = optional(bool, false)
plan_name = optional(string, "Observability-Starter-EU01")
acl = optional(list(string), [])
name = optional(string, null)
}), {})
})) | `{}` | no |
+| [observability](#input\_observability) | Observability instance configuration for the management module. Set to null to skip observability deployment. | object({
plan_name = optional(string, "Observability-Starter-EU01")
acl = optional(list(string), [])
logs_retention_days = optional(number, 30)
traces_retention_days = optional(number, 30)
metrics_retention_days = optional(number, 90)
metrics_retention_days_5m_downsampling = optional(number, 90)
metrics_retention_days_1h_downsampling = optional(number, 90)
}) | `null` | no |
+| [organization\_auditors](#input\_organization\_auditors) | List of organization auditors. | `list(string)` | `[]` | no |
+| [organization\_id](#input\_organization\_id) | Container ID of the root organization. | `string` | n/a | yes |
+| [organization\_owners](#input\_organization\_owners) | List of organization owners. | `list(string)` | `[]` | no |
+| [owner\_email](#input\_owner\_email) | Email address of the owner. Required for STACKIT resource manager. | `string` | n/a | yes |
+| [platform\_contract\_namespace](#input\_platform\_contract\_namespace) | Optional stable UUID namespace for Application handoff; defaults to the organization ID for independent CLI use. This is metadata, not an authorization boundary. | `string` | `null` | no |
+| [platform\_kubernetes](#input\_platform\_kubernetes) | Map of central, region-scoped platform Kubernetes deployments. Empty map skips deployment. | map(object({
region = string
network = optional(object({
sna_enabled = optional(bool, false)
sna_network_area_id = optional(string, null)
network_area_key = optional(string, "default")
firewall_next_hop_ip = optional(string, null)
sna_network_prefix_length = optional(number, 24)
}), {})
dns = optional(object({
enabled = optional(bool, true)
create_zones = optional(bool, true)
zones = optional(list(string), [])
gateway_api = optional(bool, true)
}), {})
observability = optional(object({
enabled = optional(bool, true)
plan_name = optional(string, "Observability-Starter-EU01")
acl = optional(list(string), [])
name = optional(string, null)
}), {})
encrypted_volumes = optional(object({
enabled = optional(bool, false)
storage_class_name = optional(string, "stackit-encrypted-premium")
kms_keyring_name = optional(string, "ske-volume-keyring")
kms_key_name = optional(string, "ske-volume-key")
kms_key_version = optional(string, "1")
}), {})
debug_bastion = optional(object({
enabled = optional(bool, false)
name = optional(string, null)
availability_zone = optional(string, null)
machine_type = optional(string, "g2i.1")
image_id = optional(string, "7b10e105-295b-4369-b6e0-567ec940a02b")
boot_volume_size = optional(number, 20)
ssh_public_key = optional(string, null)
ssh_public_key_path = optional(string, "~/.ssh/id_rsa.pub")
ssh_allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
assign_public_ip = optional(bool, true)
install_kubectl = optional(bool, true)
}), {})
role_assignments = optional(list(object({
role = string
subject = string
})), [])
cluster = object({
name = string
kubernetes_version_min = optional(string, null)
node_pools = optional(list(object({
name = string
machine_type = string
minimum = number
maximum = number
availability_zones = list(string)
allow_system_components = optional(bool, false)
volume_size = optional(number, 20)
volume_type = optional(string, "storage_premium_perf1")
os_name = optional(string, "flatcar")
labels = optional(map(string), {})
})), [
{
name = "system"
machine_type = "g3i.4"
minimum = 2
maximum = 2
availability_zones = ["eu01-1"]
allow_system_components = true
volume_size = 20
volume_type = "storage_premium_perf1"
os_name = "flatcar"
labels = {
"workload-role" = "system"
}
},
{
name = "application"
machine_type = "g3i.4"
minimum = 2
maximum = 2
availability_zones = ["eu01-2"]
allow_system_components = false
volume_size = 20
volume_type = "storage_premium_perf1"
os_name = "flatcar"
labels = {
"workload-role" = "application"
}
}
])
maintenance = optional(object({
enable_kubernetes_version_updates = optional(bool, true)
enable_machine_image_version_updates = optional(bool, true)
start = optional(string, "01:00:00Z")
end = optional(string, "02:00:00Z")
}), {})
})
})) | `{}` | no |
+| [platform\_kubernetes\_kube\_config\_override](#input\_platform\_kubernetes\_kube\_config\_override) | Optional raw kubeconfig used by kubernetes/helm platform providers when no platform\_kubernetes module output is available. | `string` | `null` | no |
+| [region](#input\_region) | STACKIT region for regional resources. | `string` | `"eu01"` | no |
+| [rm\_folder\_parent\_id](#input\_rm\_folder\_parent\_id) | ID of the parent folder under which the resource manager folders will be created. If not provided, the resource manager folders will be created under the organization. | `string` | `null` | no |
+| [rm\_folders](#input\_rm\_folders) | Map of resource manager folders to create under the root organization. Folder descriptions are unsupported; omit description or leave it empty. | map(object({
name = string
description = optional(string, null)
owner_emails = list(string)
reader_emails = list(string)
})) | {
"landing_zones_corporate": {
"name": "Landing Zones - Corporate",
"owner_emails": [],
"reader_emails": []
},
"landing_zones_public": {
"name": "Landing Zones - Public",
"owner_emails": [],
"reader_emails": []
},
"platform": {
"name": "Platform",
"owner_emails": [],
"reader_emails": []
},
"sandboxes": {
"name": "Sandboxes",
"owner_emails": [],
"reader_emails": []
}
} | no |
+| [sandboxes](#input\_sandboxes) | List of sandboxes to create. | list(object({
project_name = string
owner_emails = optional(list(string))
project_owner_email = string
})) | `[]` | no |
+| [vpn\_pre\_shared\_keys](#input\_vpn\_pre\_shared\_keys) | Pre-shared keys per connectivity.vpn.connections key, one per tunnel. Kept out of the connectivity object so the topology stays committable; supply through TF\_VAR\_vpn\_pre\_shared\_keys or a gitignored tfvars file. Minimum 20 characters. | map(object({
tunnel1 = string
tunnel2 = string
})) | `{}` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [audit\_logs\_datasource\_url](#output\_audit\_logs\_datasource\_url) | Datasource URL of the audit Logs instance, usable as a Grafana datasource. |
+| [audit\_logs\_instance\_id](#output\_audit\_logs\_instance\_id) | The ID of the Logs instance receiving audit logs. Null when audit\_logs is not configured. |
+| [audit\_telemetry\_router\_id](#output\_audit\_telemetry\_router\_id) | The ID of the Telemetry Router collecting audit logs. |
+| [connectivity\_firewall\_backup\_public\_ip](#output\_connectivity\_firewall\_backup\_public\_ip) | The public IP of the legacy default area's backup firewall. Null without connectivity.firewall.ha. |
+| [connectivity\_firewall\_next\_hop\_ip](#output\_connectivity\_firewall\_next\_hop\_ip) | The legacy default area's next hop for corporate landing-zone routes. |
+| [connectivity\_firewall\_next\_hop\_ips\_by\_area](#output\_connectivity\_firewall\_next\_hop\_ips\_by\_area) | Map of network area keys to firewall next-hop IPs. |
+| [connectivity\_firewall\_public\_ip](#output\_connectivity\_firewall\_public\_ip) | The public IP of the legacy default area's primary firewall node. |
+| [connectivity\_firewall\_public\_ips\_by\_area](#output\_connectivity\_firewall\_public\_ips\_by\_area) | Map of network area keys to primary firewall public IPs. |
+| [connectivity\_network\_area\_id](#output\_connectivity\_network\_area\_id) | The network area ID of the legacy default connectivity area. |
+| [connectivity\_network\_area\_ids\_by\_area](#output\_connectivity\_network\_area\_ids\_by\_area) | Map of connectivity network area keys to their IDs. |
+| [connectivity\_network\_area\_ids\_by\_region](#output\_connectivity\_network\_area\_ids\_by\_region) | Map of regions to network area keys and IDs for regional connectivity deployments. |
+| [connectivity\_project\_id](#output\_connectivity\_project\_id) | The project ID of the legacy default connectivity area. |
+| [connectivity\_project\_ids\_by\_area](#output\_connectivity\_project\_ids\_by\_area) | Map of connectivity network area keys to their project IDs. |
+| [connectivity\_vpn\_connection\_ids](#output\_connectivity\_vpn\_connection\_ids) | Map of hub VPN connection keys to their connection IDs. |
+| [connectivity\_vpn\_gateway\_id](#output\_connectivity\_vpn\_gateway\_id) | The ID of the legacy default area's VPN gateway. |
+| [connectivity\_vpn\_gateway\_ids\_by\_area](#output\_connectivity\_vpn\_gateway\_ids\_by\_area) | Map of network area keys to VPN gateway IDs. |
+| [connectivity\_vpn\_internal\_next\_hop\_ips](#output\_connectivity\_vpn\_internal\_next\_hop\_ips) | Network area side IPs of the hub VPN gateway tunnels. |
+| [connectivity\_vpn\_public\_ips](#output\_connectivity\_vpn\_public\_ips) | Public IPs of the hub VPN gateway tunnels. Configure the remote peer against these. |
+| [devops\_project\_id](#output\_devops\_project\_id) | The project ID of the DevOps project. |
+| [firewall\_admin\_url](#output\_firewall\_admin\_url) | Where to reach the OPNsense web GUI. Only reachable from inside the network area once the policy blocks the WAN. |
+| [governance\_folder\_ids](#output\_governance\_folder\_ids) | Map of governance folder names to their container IDs. |
+| [landing\_zone\_namespace\_demo\_samples](#output\_landing\_zone\_namespace\_demo\_samples) | Demo sample references for namespace services. |
+| [landing\_zone\_projects](#output\_landing\_zone\_projects) | Map of landing zone project IDs. |
+| [management\_bucket\_name\_tfstate](#output\_management\_bucket\_name\_tfstate) | The name of the Management tfstate object storage bucket. |
+| [management\_project\_id](#output\_management\_project\_id) | The project ID of the Management project. |
+| [platform\_contract](#output\_platform\_contract) | Non-secret Application handoff. The organization ID is the default CLI namespace; revision changes when exported references change. |
+| [platform\_kubernetes\_projects](#output\_platform\_kubernetes\_projects) | Map of platform Kubernetes projects and cluster metadata per key. |
+| [sandbox\_projects](#output\_sandbox\_projects) | The created sandbox projects. |
+
diff --git a/src/README.md b/src/README.md
index 340af17..90ac98d 100644
--- a/src/README.md
+++ b/src/README.md
@@ -1,3 +1,9 @@
+# Accelerator root module
+
+[English](README.md) | [Deutsch](README.de.md)
+
+The generated technical reference below documents this root module.
+
## Requirements
diff --git a/src/application/README.de.md b/src/application/README.de.md
new file mode 100644
index 0000000..98d60ad
--- /dev/null
+++ b/src/application/README.de.md
@@ -0,0 +1,149 @@
+
+# Separates Application-Landing-Zone-Root
+
+[English](README.md) | [Deutsch](README.de.md)
+
+Dieses Root plant **eine neue Anwendungsinstanz** unter Wiederverwendung von `../modules/landing-zone`.
+Es erstellt keine Plattformordner, SNAs, Plattformdienste oder anderen Anwendungen.
+Es funktioniert mit manuell aufgelösten CLI-Eingaben oder vom Configurator erzeugten Eingaben.
+Für die direkte CLI-Nutzung sind weder Configurator-API, Datenbank, Sitzung, Jobfreigabe
+noch Template-Engine erforderlich. Migrieren Sie bestehenden kombinierten State nicht durch
+Verzeichniswechsel oder das Löschen seiner `landing_zones`-Einträge: Dies kann destruktive Änderungen planen.
+
+Der Configurator löst Templates auf und prüft seine eigenen Identitäts-/Freigaberichtlinien.
+CLI-Nutzer geben die aufgelösten Werte direkt an, einschließlich Eigentümer und
+Diensteinstellungen. In beiden Betriebsarten setzen STACKIT IAM und Backend-Richtlinien
+Berechtigungen durch; Eingabemetadaten, Template-Versionen und Herkunftslabels sind keine IAM-Grenze.
+
+Das wiederverwendete Modul erstellt ein Projekt, ein Automatisierungsdienstkonto mit Schlüssel,
+Object-Storage-Buckets/-Zugangsdaten sowie die ausgewählten Secrets-Manager- und
+Observability-Dienste. Corporate-Instanzen referenzieren eine plattformeigene SNA und
+erstellen ihr Projektnetzwerk/Routing. Public-Instanzen können optional mit
+`network_enabled` ein lokales Projektnetzwerk und optional mit `network_prefix_length`
+eine Präfixlänge festlegen. Dieses Netzwerk wird nicht über die SNA geroutet; bisherige
+öffentliche Eingaben ohne diese Option erstellen weiterhin kein Netzwerk. Templates mit
+Schema 2 legen diese Werte fest, statt Überschreibungen bei Bestellungen zu erlauben.
+Das Root stellt eine nicht geheime `project_network`-Referenz bereit, keine
+Dienstzugangsdaten. Ein lokales Netzwerk qualifiziert sein privates CIDR nicht als
+Observability-ACL-Quelle. Dieser Umfang muss bei Template-Veröffentlichung und Plan-Prüfung
+sichtbar sein. Generierte Zugangsdaten verbleiben im geschützten Application-State;
+sie werden nicht über Root-Ausgaben offengelegt.
+
+
+## Unabhängiger zweiphasiger CLI-Ablauf
+
+
+### Phase 1: Plattform
+
+Verwenden Sie das normale [Accelerator-Bootstrap-Verfahren](../../docs/getting-started.de.md).
+Konfigurieren Sie für eine **neue**, reine Plattforminstallation ausdrücklich
+`landing_zones = {}`, `sandboxes = []` und `landing_zone_namespace_services = {}`
+vor dem ersten Apply. Behalten Sie die erforderlichen Plattformdienste und
+Governance-Ordner bei. Das bestehende Root bleibt mit kombinierten Installationen
+kompatibel; ein separates physisches Plattform-Root und die Migration bestehender
+States werden hier nicht eingeführt.
+
+Exportieren Sie nach Prüfung und Anwendung des Plattformplans nur die nicht geheime Übergabe:
+
+```sh
+tofu -chdir=src output -json platform_contract > platform-contract.json
+```
+
+Diese enthält Referenzen auf Organisation, Ordner, Region, SNA, Next-Hop und
+Nameserver, nicht den vollständigen Plattform-State oder Zugangsdaten. Ihre Revision ist
+eine deterministische UUID, abgeleitet aus Namespace und exportierten Referenzen.
+Geänderte Referenzen ändern die Revision; bewahren Sie das geprüfte Artefakt mit der
+Anwendungskonfiguration auf. `tenant_id` ist ein logischer Namespace, standardmäßig
+die Organisations-UUID. Das optionale `platform_contract_namespace` erlaubt eine andere
+stabile UUID, beispielsweise den vorgesehenen Configurator-Workspace. Für die
+CLI-Nutzung ist keine vom Configurator ausgestellte UUID erforderlich.
+Öffentliche Ziele sind `public-eu01`/`public-eu02`; Corporate-Zielschlüssel enthalten
+einen stabilen Hash des Area-Schlüssels, damit beliebige Area-Namen keine ungültigen
+oder kollidierenden normalisierten Zielnamen erzeugen können. Nur vorhandene
+Governance-Ordner/SNAs werden exportiert. Ein exportiertes Ziel ist eine
+Ressourcenreferenz, keine Berechtigung zur Bereitstellung dort.
+
+
+### Phase 2: Anwendung
+
+Beginnen Sie mit [dem aufgelösten JSON-Beispiel](examples/application.tfvars.json.example).
+Alle Beispiel-IDs sind fiktiv. Ersetzen Sie `platform_contract` durch das Artefakt aus
+Phase 1 und füllen Sie die Anwendungswerte manuell aus. Template-Rendering ist nicht
+erforderlich. Setzen Sie `application.tenant_id` und `application.platform_revision`
+exakt aus diesem Artefakt; wählen Sie einen seiner Zielschlüssel. Vergeben Sie
+`instance_id` einmalig (beispielsweise `uuidgen | tr '[:upper:]' '[:lower:]'`) und
+halten Sie sie stabil. `template_id`/`template_version` dokumentieren die gewählte
+Ausgangsbasis; ein veröffentlichtes Configurator-Template ist nicht erforderlich.
+Eigentümer, Rollen, Netzwerk- und Diensteinstellungen müssen nach den
+Bereitstellungsrichtlinien Ihrer Organisation geprüft werden.
+
+Stellen Sie **vor** dem ersten Application-Plan einen Application-State-Bucket bereit.
+Verwenden Sie [das Backend-Beispiel](examples/backend.hcl.example), ersetzen Sie
+Bucket/Schlüssel und halten Sie den Schlüssel
+`applications/map(object({
dns_name = string
network_area_key = optional(string, "default")
name = optional(string, null)
contact_email = optional(string, null)
type = optional(string, "primary")
acl = optional(string, null)
description = optional(string, null)
default_ttl = optional(number, 3600)
})) | `{}` | no |
+| [firewall](#input\_firewall) | Firewall configuration. Set to null to skip firewall deployment (network area and routing are still created). lan\_network\_range and wan\_network\_range must be CIDRs within the network area range. lan\_ip and wan\_ip are optional; when omitted, the 5th address of the respective prefix is used (STACKIT reserves the first usable address as the gateway). Set ha for an active/passive CARP pair; the LAN VIP then replaces the primary's LAN IP as the platform next hop. | object({
zone = string
flavor = string
name = string
volume_performance_class = optional(string, "storage_premium_perf4")
volume_size = optional(number, 16)
lan_network_range = string
wan_network_range = string
lan_ip = optional(string, null)
wan_ip = optional(string, null)
# Active/passive HA. Adds a second appliance in backup_zone plus a CARP virtual IP on
# the LAN that becomes the platform next hop. CARP runs in unicast mode (OPNsense
# >= 24.7) because the STACKIT fabric does not deliver advertisements sourced from
# the shared virtual MAC, so multicast CARP splits the brain.
# Node-local settings are pushed by scripts/configure-ha.sh during apply.
ha = optional(object({
backup_zone = string
backup_name = optional(string, null) # defaults to "-backup"
backup_lan_ip = optional(string, null) # defaults to the 6th address of lan_network_range
backup_wan_ip = optional(string, null) # defaults to the 6th address of wan_network_range
lan_vip = optional(string, null) # defaults to the 7th address of lan_network_range
vhid = optional(number, 1)
}), null)
}) | `null` | no |
+| [firewall\_admin\_endpoint](#input\_firewall\_admin\_endpoint) | Base URL the HA configuration logs into the primary appliance with. Defaults to the primary's public IP, which is the only address reachable from outside the network area. Set it to the LAN address when OpenTofu runs inside the area, matching firewall\_config.endpoint. The backup node is always configured over its own public IP: before HA exists it has no other reachable address. | `string` | `null` | no |
+| [firewall\_admin\_password](#input\_firewall\_admin\_password) | Password of firewall\_admin\_username. Defaults to the password baked into the STACKIT OPNsense image. Only used when firewall.ha is set. | `string` | `"STACKIT123!"` | no |
+| [firewall\_admin\_username](#input\_firewall\_admin\_username) | Appliance login used to push the node-local HA settings. The STACKIT OPNsense image ships with root. Only used when firewall.ha is set. | `string` | `"root"` | no |
+| [firewalls](#input\_firewalls) | Map of network area keys to firewall configuration. Use this for multiple network areas; keys must match network\_areas. | map(object({
zone = string
flavor = string
name = string
volume_performance_class = optional(string, "storage_premium_perf4")
volume_size = optional(number, 16)
lan_network_range = string
wan_network_range = string
lan_ip = optional(string, null)
wan_ip = optional(string, null)
ha = optional(object({
backup_zone = string
backup_name = optional(string, null)
backup_lan_ip = optional(string, null)
backup_wan_ip = optional(string, null)
lan_vip = optional(string, null)
vhid = optional(number, 1)
}), null)
})) | `null` | no |
+| [labels](#input\_labels) | Additional labels to apply to all resources. | `map(string)` | `{}` | no |
+| [naming\_pattern](#input\_naming\_pattern) | Naming prefix for all resources in this module, e.g. "myco-pltfm-hub-prod". | `string` | n/a | yes |
+| [network\_areas](#input\_network\_areas) | Map of network area keys to configuration. Keys are stable references for DNS zones, landing zones, and outputs. | map(object({
name = optional(string, null)
ranges = list(string)
transfer_network = string
max_prefix_length = optional(number, 28)
min_prefix_length = optional(number, 24)
default_prefix_length = optional(number, 28)
default_nameservers = optional(list(string), null)
})) | `{}` | no |
+| [organization\_id](#input\_organization\_id) | Organization ID, required for network area and route configuration. | `string` | n/a | yes |
+| [owner\_email](#input\_owner\_email) | Email address of the owner for the project. Required for STACKIT resource manager. | `string` | n/a | yes |
+| [parent\_container\_id](#input\_parent\_container\_id) | Parent container ID (folder or organization) where the project will be created. | `string` | n/a | yes |
+| [project\_name](#input\_project\_name) | Name of the STACKIT project to create. Falls back to naming\_pattern if not set. | `string` | `null` | no |
+| [region](#input\_region) | STACKIT region the network area region is created in. Also selects the default resolvers when network\_area.default\_nameservers is unset. | `string` | `"eu01"` | no |
+| [role\_assignments](#input\_role\_assignments) | List of role assignments for the project. Subject can be a user email or service account email. | list(object({
role = string
subject = string
})) | `[]` | no |
+| [vpn](#input\_vpn) | IPsec VPN gateway for the hub, attached to the network area through the connectivity project. Set to null to skip. The gateway is HA: it terminates two tunnels in separate availability zones, each with its own public IP. Connections are created in a second apply once the remote peer addresses are known. Supports POLICY\_BASED and ROUTE\_BASED routing. | object({
display_name = optional(string, null)
plan_id = optional(string, "p100")
routing_type = optional(string, "ROUTE_BASED")
availability_zones = object({
tunnel1 = string
tunnel2 = string
})
connections = optional(map(object({
display_name = optional(string, null)
enabled = optional(bool, true)
local_subnets = optional(list(string), null)
remote_subnets = optional(list(string), null)
static_routes = optional(list(string), null)
tunnel1 = object({
remote_address = string
peering = optional(object({
local_address = string
remote_address = string
}), null)
phase1 = optional(object({
encryption_algorithms = optional(list(string), ["aes256"])
integrity_algorithms = optional(list(string), ["sha2_384"])
dh_groups = optional(list(string), ["ecp384"])
rekey_time = optional(number, null)
}), {})
phase2 = optional(object({
encryption_algorithms = optional(list(string), ["aes256"])
integrity_algorithms = optional(list(string), ["sha2_384"])
dh_groups = optional(list(string), ["ecp384"])
rekey_time = optional(number, null)
dpd_action = optional(string, null)
start_action = optional(string, null)
}), {})
})
tunnel2 = object({
remote_address = string
peering = optional(object({
local_address = string
remote_address = string
}), null)
phase1 = optional(object({
encryption_algorithms = optional(list(string), ["aes256"])
integrity_algorithms = optional(list(string), ["sha2_384"])
dh_groups = optional(list(string), ["ecp384"])
rekey_time = optional(number, null)
}), {})
phase2 = optional(object({
encryption_algorithms = optional(list(string), ["aes256"])
integrity_algorithms = optional(list(string), ["sha2_384"])
dh_groups = optional(list(string), ["ecp384"])
rekey_time = optional(number, null)
dpd_action = optional(string, null)
start_action = optional(string, null)
}), {})
})
})), {})
}) | `null` | no |
+| [vpn\_pre\_shared\_keys](#input\_vpn\_pre\_shared\_keys) | Pre-shared keys per VPN connection key, one per tunnel. Kept separate from var.vpn so the connection topology stays committable; supply through TF\_VAR\_vpn\_pre\_shared\_keys or a gitignored tfvars file. Minimum 20 characters. | map(object({
tunnel1 = string
tunnel2 = string
})) | `{}` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [dns\_zone\_dns\_names](#output\_dns\_zone\_dns\_names) | Map of DNS zone keys to their DNS names |
+| [dns\_zone\_ids](#output\_dns\_zone\_ids) | Map of DNS zone keys to their zone IDs |
+| [firewall\_backup\_public\_ip](#output\_firewall\_backup\_public\_ip) | Map of network area keys to backup firewall WAN public IPs. Empty without HA. |
+| [firewall\_cluster\_lan\_ips](#output\_firewall\_cluster\_lan\_ips) | Map of network area keys to LAN addresses of firewall HA pairs. Empty without HA. |
+| [firewall\_next\_hop\_ip](#output\_firewall\_next\_hop\_ip) | Map of network area keys to the default-route next hop. The CARP LAN VIP under HA, otherwise the firewall LAN IP. |
+| [firewall\_public\_ip](#output\_firewall\_public\_ip) | Map of network area keys to primary firewall WAN public IPs. |
+| [network\_area\_id](#output\_network\_area\_id) | The ID of the created network area. |
+| [network\_area\_nameservers](#output\_network\_area\_nameservers) | Resolvers configured as the network area default, either from network\_area.default\_nameservers or the STACKIT resolvers of the region. |
+| [project\_container\_id](#output\_project\_container\_id) | The container ID of the created STACKIT project. |
+| [project\_id](#output\_project\_id) | The project ID of the created STACKIT project. |
+| [project\_name](#output\_project\_name) | The name of the created STACKIT project. |
+| [vpn\_connection\_ids](#output\_vpn\_connection\_ids) | Map of network-area/connection keys to their VPN connection IDs. |
+| [vpn\_gateway\_id](#output\_vpn\_gateway\_id) | Map of network area keys to VPN gateway IDs. |
+| [vpn\_internal\_next\_hop\_ips](#output\_vpn\_internal\_next\_hop\_ips) | Map of VPN tunnel names to their network area side IP. Ping targets to verify a tunnel carries traffic into the SNA. |
+| [vpn\_public\_ips](#output\_vpn\_public\_ips) | Map of VPN tunnel names to their public IP. These are the addresses the remote peer has to be configured against. |
+
+
+
+## Mehrere Network Areas
+
+Übergeben Sie `network_areas` als Map mit aussagekräftigen Schlüsseln wie `finance`, `research` oder `tenant_a`. Für jeden Schlüssel werden ein separates Connectivity-Projekt, eine Network Area, eine Routingtabelle und ein optionales VPN-Gateway erstellt. Referenzieren Sie einen Schlüssel über `dns_zones.[| no | +| [company\_name](#input\_company\_name) | Name of the company folder to create. | `string` | n/a | yes | +| [git\_flavor](#input\_git\_flavor) | The flavor of the Git instance. | `string` | `null` | no | +| [labels](#input\_labels) | Additional labels to apply to all folders. | `map(string)` | `{}` | no | +| [naming\_pattern](#input\_naming\_pattern) | Naming prefix for all resources in this module, e.g. "myco-pltfm-hub-prod". | `string` | n/a | yes | +| [network\_area\_id](#input\_network\_area\_id) | Network Area ID to deploy resources into. Required if network is enabled. | `string` | `null` | no | +| [owner\_email](#input\_owner\_email) | Email address of the owner for the folders. Required for STACKIT resource manager. | `string` | n/a | yes | +| [parent\_container\_id](#input\_parent\_container\_id) | Parent container ID (folder or organization) where the project will be created. | `string` | n/a | yes | +| [project\_name](#input\_project\_name) | Name of the STACKIT project to create. | `string` | `null` | no | +| [role\_assignments](#input\_role\_assignments) | List of role assignments for the project. Subject can be a user email or service account email. |
"0.0.0.0/0"
]
list(object({
role = string
subject = string
})) | `[]` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [project\_container\_id](#output\_project\_container\_id) | The container ID of the created STACKIT project. |
+| [project\_id](#output\_project\_id) | The project ID of the created STACKIT project. |
+| [project\_name](#output\_project\_name) | The name of the created STACKIT project. |
+
diff --git a/src/modules/devops/README.md b/src/modules/devops/README.md
index be23d6d..81dfb06 100644
--- a/src/modules/devops/README.md
+++ b/src/modules/devops/README.md
@@ -1,3 +1,9 @@
+# devops
+
+[English](README.md) | [Deutsch](README.de.md)
+
+The generated technical reference below documents this module.
+
## Requirements
diff --git a/src/modules/firewall-config/README.de.md b/src/modules/firewall-config/README.de.md
new file mode 100644
index 0000000..6642fcc
--- /dev/null
+++ b/src/modules/firewall-config/README.de.md
@@ -0,0 +1,250 @@
+# firewall-config
+
+[English](README.md) | [Deutsch](README.de.md)
+
+Überträgt Firewallregeln, NAT und internetseitige Härtung über die API an eine
+OPNsense-Appliance mithilfe des Providers
+[`browningluke/opnsense`](https://registry.terraform.io/providers/browningluke/opnsense/latest/docs).
+
+Die Appliance selbst wird vom Modul `connectivity` bereitgestellt (`connectivity.firewall`);
+dieses Modul konfiguriert sie nur. Binden Sie es über die Root-Variable `firewall_config`
+ein, statt es direkt aufzurufen – siehe `src/config/hub-and-spoke-firewall.tfvars`.
+
+
+## Verwaltete Bestandteile
+
+- **Aliase** — benannte Netzwerk-, Host-, Port- oder FQDN-Gruppen, die Regeln und NAT-Einträge referenzieren.
+- **Statische Routen** — teilen der Appliance mit, welche Präfixe hinter welchem Gateway liegen.
+- **Filterregeln** — pass, block oder reject, pro Schnittstelle oder als Floating-Regel.
+- **Outbound NAT** — ausgehender Verkehr der Landing Zones hinter der öffentlichen Adresse der Firewall.
+- **Portweiterleitungen** — eingehende Freigaben aus dem Internet mit Schutzprüfung (siehe unten).
+
+Das Modul enthält keine eigene Richtlinie. Jede Regel kommt vom Aufrufer, sodass
+die wirksame Konfiguration immer an einer Stelle lesbar ist, statt auf tfvars und
+Modulinterna verteilt zu sein.
+
+
+## Regelreihenfolge
+
+Alles hier Geschriebene landet im *Automation*-Regelsatz von OPNsense, der vor den
+unter **Firewall → Rules** konfigurierten Regeln ausgewertet wird. Die vollständige Reihenfolge:
+
+1. Systemregeln (Anti-Lockout, DHCP, Loopback)
+2. Automatisierte **und** manuelle Floating-Regeln — Priorität `200000`
+3. Automatisierte **und** manuelle Schnittstellen*gruppen*regeln — `300000`
+4. **Automatisierte** Regeln einzelner Schnittstellen — `400000`
+5. **Manuelle** Regeln einzelner Schnittstellen
+
+Zwei Folgen für diese Topologie:
+
+- Die `Default allow LAN to any rule` des Images ist eine manuelle Schnittstellenregel
+ und steht daher in Schritt 5. Eine hier auf `lan` geschriebene `block`-Regel wird
+ zuerst erreicht und gewinnt. Dadurch ist eine Default-Deny-Richtlinie auf der
+ LAN-Seite möglich, ohne das Image anzupassen.
+- Innerhalb eines Schritts gewinnt die niedrigste `sequence`. Regeln haben standardmäßig
+ `quick = true`, sodass der erste Treffer entscheidet. Lassen Sie Lücken zwischen Sequenznummern.
+
+`interfaces = []` erzeugt eine Floating-Regel (Schritt 2), die einzige Möglichkeit,
+einer bereits im Image enthaltenen Floating-Regel zuvorzukommen.
+
+
+## Routen
+
+Beide Appliance-Schnittstellen sind DHCP-Clients in einem `/28`; OPNsense kennt
+anfangs daher nur diese beiden Präfixe und eine Standardroute über WAN. Die Landing
+Zones liegen weiter innerhalb der Network Area und sind über das LAN-Gateway
+erreichbar, was ausdrücklich angegeben werden muss:
+
+```hcl
+routes = {
+ network-area-via-lan = {
+ network = "10.0.0.0/16"
+ gateway = "LAN_DHCP"
+ }
+}
+```
+
+Ohne diese Route verlässt Verkehr zu Spokes die Appliance über WAN und tritt von
+außen wieder in die Network Area ein. Er erreicht die Spokes weiterhin, da die
+WAN-Routingtabelle Systemrouten enthält. Allerdings wird dadurch auch
+Landing-Zone-zu-Landing-Zone-Verkehr über die WAN-Schnittstelle geführt, wo die
+Egress-NAT-Regel die Quelladresse umschreibt. Der empfangende Spoke sieht dann die
+Firewall statt des sendenden Spokes; quellbasierte Regeln verlieren ihre Aussagekraft.
+
+`gateway` muss ein bereits auf der Appliance vorhandenes Gateway benennen. Das
+STACKIT-Image enthält `LAN_DHCP` und `WAN_DHCP`, wobei `WAN_DHCP` als Standard markiert ist.
+
+
+## Domainbasierte Regeln
+
+Ein Alias mit `type = "host"` akzeptiert FQDNs, die die Appliance zeitgesteuert erneut
+auflöst. Eine darauf verweisende Regel folgt daher Adressänderungen.
+`type = "urltable"` lädt stattdessen eine Präfixliste von einer URL und benötigt
+`update_freq` in Tagen.
+
+Dies bleibt Paketfilterung anhand aufgelöster Adressen, keine namensbasierte Filterung.
+Ein Client, der denselben Namen zu einer der Appliance noch unbekannten Adresse
+auflöst, wird blockiert. Alles andere, das eine bereits aufgelöste Adresse teilt,
+wird durchgelassen. Für eine harte Garantie verwenden Sie einen Mirror mit stabiler
+Adresse oder schalten Sie einen Proxy in den Pfad.
+
+
+## Web-GUI gegenüber dem Internet schließen
+
+Das STACKIT-Image enthält eine **Floating**-Regel namens `Initial webUI Access Rule`,
+die TCP/443 von jeder Quelle zur Firewall selbst zulässt. Nach einer einfachen
+Bereitstellung antwortet die Web-GUI auf der öffentlichen IP.
+
+Zum Überschreiben sind zwei Regeln erforderlich, beide ebenfalls als Floating-Regeln:
+OPNsense wertet Floating-Regeln (Prioritätsgruppe 200000) vor schnittstellengebundenen
+Regeln (400000) aus. Eine WAN-gebundene Regel kann deshalb niemals zuerst greifen.
+`src/config/hub-and-spoke-firewall.tfvars` enthält das ausgearbeitete Beispiel:
+
+| Sequenz | Regel | Wirkung |
+| --- | --- | --- |
+| 10 | pass TCP → `(self):443` von einem Management-Alias | erhält den Administrationszugriff |
+| 20 | block TCP → `(self):443` von any, floating | schließt die GUI für alle anderen |
+
+An einer echten Appliance überprüft: Eine erlaubte Quelle erhielt `HTTP 200`,
+während eine nicht gelistete Internetquelle verworfen wurde.
+
+> [!WARNING]
+> Der Management-Alias muss das System, das OpenTofu ausführt, enthalten, bevor die
+> Blockregel angewendet wird. Andernfalls trennt das Apply, das die GUI schließt,
+> zugleich die für den nächsten Lauf erforderliche Verbindung; die Wiederherstellung
+> erfolgt dann über die serielle Konsole.
+
+> [!NOTE]
+> Eine Regel „RFC1918 auf WAN blockieren“ ist ein üblicher Härtungsreflex, passt
+> jedoch nicht ungeprüft zu dieser Topologie: Die WAN-Schnittstelle der Appliance
+> liegt auf einem Network-Area-Präfix (`wan_network_range`), nicht an einer
+> Internetgrenze. Eine solche Regel kann legitimen internen Verkehr verwerfen.
+> Testen Sie sie vor der Übernahme.
+
+## NAT
+
+OPNsense unterteilt NAT in drei Bereiche. Das Modul deckt die ersten beiden ab:
+
+| OPNsense-GUI | NAT-Richtung | Modulvariable |
+| --- | --- | --- |
+| Firewall → NAT → Outbound | Quell-NAT | `outbound_nat` |
+| Firewall → NAT → Port Forward | Ziel-NAT, also eingehend | `port_forwards` |
+| Firewall → NAT → One-to-One | bidirektional 1:1 | nicht verfügbar |
+
+Ohne mindestens einen `outbound_nat`-Eintrag haben die Landing Zones keine
+Egress-Identität, da ihre Standardroute an dieser Appliance endet. Übersetzen Sie
+auf `wanip`: Die öffentliche STACKIT-IP ist an die WAN-Schnittstellenadresse
+gebunden. Jede andere Quelladresse wird daher an der Netzwerkgrenze nicht übersetzt
+und erreicht niemals das Internet.
+
+Das Image belässt **Firewall → NAT → Outbound** auf `automatic`; die API kann
+diesen Modus nicht ändern. Das muss sie auch nicht: Hier erzeugte Regeln sind
+Automatisierungsregeln, die unabhängig vom Modus angewendet werden und vor allen
+Einträgen unter `Firewall → NAT` greifen. Die automatisch von der Appliance
+erzeugten Regeln decken nur ihre eigenen direkt verbundenen `/28`s ab und
+kollidieren daher niemals mit Landing-Zone-Verkehr.
+
+`disable_nat = true` markiert Verkehr als unübersetzt *und beendet dafür die
+Outbound-NAT-Verarbeitung*. Ein No-NAT-Eintrag funktioniert daher nur, wenn seine
+`sequence` niedriger ist als die des zu übersteuernden Eintrags.
+
+> [!IMPORTANT]
+> Eine Portweiterleitung schreibt nur die Zieladresse um. Die GUI bietet an,
+> gleichzeitig die passende Filterregel zu erstellen; der Provider hat diese
+> Option nicht. Jede Weiterleitung benötigt daher eine eigene Pass-Regel in
+> `rules`, die den übersetzten Verkehr auf der WAN-Schnittstelle erlaubt.
+> Ohne sie wird das Paket übersetzt und anschließend verworfen.
+
+Jeder Eintrag in `port_forwards` öffnet einen Pfad aus dem Internet. Daher weist
+die Root-Variable eine aktivierte Weiterleitung zurück, deren `source_net`
+weiterhin `any` ist. Grenzen Sie es ein oder deaktivieren Sie den Eintrag.
+
+NAT-Beschreibungen akzeptieren nur alphanumerische Zeichen, Leerzeichen und Punkte.
+Map-Schlüssel in Kebab-Case werden automatisch bereinigt, wenn keine ausdrückliche
+`description` angegeben ist.
+
+
+## Erreichbarkeit
+
+Die API läuft auf der Web-GUI der Appliance an der LAN-Adresse innerhalb der
+Network Area. Das System, das OpenTofu ausführt, muss sie von dort erreichen können –
+eine Bastion im Hub, eine Landing Zone oder das Site-to-Site-VPN. Ein Runner im
+öffentlichen Internet funktioniert nur, solange die GUI noch öffentlich erreichbar
+ist; genau das entfernt die Blockregel.
+
+
+## Provider-Auswahl
+
+Der generische Provider
+[`Mastercard/restapi`](https://registry.terraform.io/providers/Mastercard/restapi/latest/docs)
+kann diese API ebenfalls ansteuern. OPNsense beantwortet Lesezugriffe jedoch in
+einer erweiterten Form (jedes Auswahlfeld wird zu einem Optionsobjekt mit
+`selected`-Flags), die nie den geschriebenen Daten entspricht. Unterdrückt man die
+daraus entstehende permanente Differenz mit `ignore_all_server_changes`, werden
+auch Konfigurationsänderungen unterdrückt. Objekte werden dadurch faktisch nur
+einmal beschreibbar: Das Ergänzen eines CIDRs zu einem Alias meldet `No changes`,
+während die Appliance den alten Inhalt behält.
+
+`browningluke/opnsense` bildet die beiden Formen intern aufeinander ab, sodass
+In-place-Updates und Drift-Erkennung funktionieren. Er wird von der Community
+gepflegt, und sein Autor rät vom Produktionseinsatz ab. Wird ein nicht abgedeckter
+Endpunkt benötigt, bleibt der generische Provider für dieses konkrete Objekt ein
+brauchbarer Ausweg.
+
+Der folgende generatorverwaltete technische Block bleibt unverändert auf Englisch;
+maßgeblich ist die [kanonische englische Referenz](README.de.md).
+
+
+## Requirements
+
+| Name | Version |
+| ---- | ------- |
+| [terraform](#requirement\_terraform) | >= 1.11 |
+| [opnsense](#requirement\_opnsense) | >= 0.26.0 |
+
+## Providers
+
+| Name | Version |
+| ---- | ------- |
+| [opnsense](#provider\_opnsense) | >= 0.26.0 |
+| [terraform](#provider\_terraform) | n/a |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+| ---- | ---- |
+| [opnsense_firewall_alias.this](https://registry.terraform.io/providers/browningluke/opnsense/latest/docs/resources/firewall_alias) | resource |
+| [opnsense_firewall_category.this](https://registry.terraform.io/providers/browningluke/opnsense/latest/docs/resources/firewall_category) | resource |
+| [opnsense_firewall_filter.this](https://registry.terraform.io/providers/browningluke/opnsense/latest/docs/resources/firewall_filter) | resource |
+| [opnsense_firewall_nat.this](https://registry.terraform.io/providers/browningluke/opnsense/latest/docs/resources/firewall_nat) | resource |
+| [opnsense_firewall_nat_port_forward.this](https://registry.terraform.io/providers/browningluke/opnsense/latest/docs/resources/firewall_nat_port_forward) | resource |
+| [opnsense_route.this](https://registry.terraform.io/providers/browningluke/opnsense/latest/docs/resources/route) | resource |
+| [terraform_data.ha_sync](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+| ---- | ----------- | ---- | ------- | :------: |
+| [admin\_password](#input\_admin\_password) | Password of admin\_username, used by the HA sync trigger. Defaults to the password baked into the STACKIT OPNsense image. Only used when ha\_sync is set. | `string` | `"STACKIT123!"` | no |
+| [admin\_username](#input\_admin\_username) | Appliance login used by the HA sync trigger. Only used when ha\_sync is set. | `string` | `"root"` | no |
+| [aliases](#input\_aliases) | Firewall aliases keyed by alias name. Reference an alias from a rule, route or NAT entry by using its key wherever a network is expected. type = "host" also accepts FQDNs, which the appliance re-resolves periodically. | map(object({
type = optional(string, "network")
enabled = optional(bool, true)
description = optional(string, null)
content = optional(list(string), [])
update_freq = optional(number, null)
stats = optional(bool, false)
})) | `{}` | no |
+| [category\_name](#input\_category\_name) | OPNsense category every object created by this module is tagged with, so managed objects are recognisable in the GUI. | `string` | `"landing-zone"` | no |
+| [endpoint](#input\_endpoint) | Base URL of the OPNsense API on the primary node, e.g. https://10.0.2.4. Only used by the HA sync trigger; the provider itself is configured in the root module. | `string` | `null` | no |
+| [ha\_sync](#input\_ha\_sync) | Push this policy to the HA peer after every change (POST /api/core/hasync\_status/restart\_all on the primary). Required for the active/passive CARP pair: OPNsense's own config sync never fires on API writes, so without it the backup runs an empty ruleset. Enabled automatically when connectivity.firewall.ha is set. | `bool` | `false` | no |
+| [outbound\_nat](#input\_outbound\_nat) | Additional outbound NAT rules keyed by name. target\_ip accepts an address, an alias, or map(object({
sequence = optional(number, 200)
enabled = optional(bool, true)
interface = optional(string, "wan")
protocol = optional(string, "any")
ip_protocol = optional(string, "inet")
source_net = optional(string, "any")
destination_net = optional(string, "any")
target_ip = optional(string, "wanip")
disable_nat = optional(bool, false)
log = optional(bool, false)
description = optional(string, null)
})) | `{}` | no |
+| [port\_forwards](#input\_port\_forwards) | Inbound port forwards from the internet keyed by name. Every entry punches a hole through the WAN, so keep the list short and set source\_net where possible. | map(object({
sequence = optional(number, 100)
enabled = optional(bool, true)
interfaces = optional(list(string), ["wan"])
protocol = optional(string, "TCP")
ip_protocol = optional(string, "inet")
source_net = optional(string, "any")
destination_net = optional(string, "wanip")
destination_port = string
target_ip = string
target_port = optional(string, null)
nat_reflection = optional(string, "default")
log = optional(bool, true)
description = optional(string, null)
})) | `{}` | no |
+| [routes](#input\_routes) | Static routes keyed by name. gateway must name a gateway that exists on the appliance; the STACKIT image ships LAN\_DHCP and WAN\_DHCP. | map(object({
enabled = optional(bool, true)
network = string
gateway = string
description = optional(string, null)
})) | `{}` | no |
+| [rules](#input\_rules) | Firewall filter rules keyed by name. An empty interfaces list creates a floating rule, which OPNsense evaluates before interface-bound rules. | map(object({
sequence = optional(number, 100)
enabled = optional(bool, true)
action = optional(string, "pass")
direction = optional(string, "in")
interfaces = optional(list(string), ["lan"])
protocol = optional(string, "any")
ip_protocol = optional(string, "inet")
quick = optional(bool, true)
source_net = optional(string, "any")
source_port = optional(string, null)
source_invert = optional(bool, false)
destination_net = optional(string, "any")
destination_port = optional(string, null)
destination_invert = optional(bool, false)
log = optional(bool, false)
description = optional(string, null)
})) | `{}` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [category](#output\_category) | Name and UUID of the category every object created by this module is tagged with. Filtering on the name in the OPNsense GUI shows exactly what OpenTofu manages and nothing else. |
+| [managed\_objects](#output\_managed\_objects) | Names of the objects this module holds on the appliance, by kind. Read from the resources rather than the inputs, so it reflects what was actually pushed. |
+| [rule\_evaluation\_order](#output\_rule\_evaluation\_order) | Filter rules in the order the appliance evaluates them. Every rule defaults to quick, so the first match decides and this is the list to read when a rule does not behave as expected. Derived from the inputs, so it is reviewable in a plan before anything is pushed. |
+
diff --git a/src/modules/firewall-config/README.md b/src/modules/firewall-config/README.md
index f8f9286..1c0bb37 100644
--- a/src/modules/firewall-config/README.md
+++ b/src/modules/firewall-config/README.md
@@ -1,5 +1,7 @@
# firewall-config
+[English](README.md) | [Deutsch](README.de.md)
+
Pushes firewall rules, NAT and internet-facing hardening to an OPNsense appliance through
its API, using the [`browningluke/opnsense`](https://registry.terraform.io/providers/browningluke/opnsense/latest/docs)
provider.
diff --git a/src/modules/governance/README.de.md b/src/modules/governance/README.de.md
new file mode 100644
index 0000000..dad61ed
--- /dev/null
+++ b/src/modules/governance/README.de.md
@@ -0,0 +1,59 @@
+# governance
+
+[English](README.md) | [Deutsch](README.de.md)
+
+Die folgende generierte technische Referenz dokumentiert dieses Modul.
+Der generatorverwaltete technische Block bleibt unverändert auf Englisch;
+maßgeblich ist die [kanonische englische Referenz](README.de.md).
+
+
+## Requirements
+
+| Name | Version |
+| ---- | ------- |
+| [terraform](#requirement\_terraform) | >= 1.11 |
+| [stackit](#requirement\_stackit) | >= 0.114.0 |
+| [time](#requirement\_time) | >= 0.14.1 |
+
+## Providers
+
+| Name | Version |
+| ---- | ------- |
+| [stackit](#provider\_stackit) | 0.106.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+| ---- | ---- |
+| [stackit_authorization_folder_role_assignment.owners](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_folder_role_assignment) | resource |
+| [stackit_authorization_folder_role_assignment.readers](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_folder_role_assignment) | resource |
+| [stackit_authorization_organization_role_assignment.auditor](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_organization_role_assignment) | resource |
+| [stackit_authorization_organization_role_assignment.owner](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_organization_role_assignment) | resource |
+| [stackit_authorization_project_custom_role.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_project_custom_role) | resource |
+| [stackit_resourcemanager_folder.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/resourcemanager_folder) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+| ---- | ----------- | ---- | ------- | :------: |
+| [custom\_roles](#input\_custom\_roles) | List of custom roles to create at the organization level. | list(object({
name = string
description = string
permissions = list(string)
})) | `[]` | no |
+| [labels](#input\_labels) | Additional labels to apply to all folders. | `map(string)` | `{}` | no |
+| [organization\_auditors](#input\_organization\_auditors) | List of organization role assignments for organization auditors. | `list(string)` | `[]` | no |
+| [organization\_id](#input\_organization\_id) | Container ID of the root folder or organization under which the company folder will be created. | `string` | n/a | yes |
+| [organization\_owners](#input\_organization\_owners) | List of organization role assignments for organization owners. | `list(string)` | `[]` | no |
+| [owner\_email](#input\_owner\_email) | Email address of the owner for the folders. Required for STACKIT resource manager. | `string` | n/a | yes |
+| [rm\_folder\_parent\_id](#input\_rm\_folder\_parent\_id) | ID of the parent folder under which the resource manager folders will be created. If not provided, the resource manager folders will be created under the organization. | `string` | `null` | no |
+| [rm\_folders](#input\_rm\_folders) | Map of folder keys to folder configuration. Each folder has a display name and optional lists of owner and reader subjects. Descriptions are unsupported; omit description or leave it empty. | map(object({
name = string
description = optional(string, null)
owner_emails = optional(list(string), [])
reader_emails = optional(list(string), [])
})) | {
"landing_zones_corporate": {
"name": "Landing Zones - Corporate",
"owner_emails": [],
"reader_emails": []
},
"landing_zones_public": {
"name": "Landing Zones - Public",
"owner_emails": [],
"reader_emails": []
},
"platform": {
"name": "Platform",
"owner_emails": [],
"reader_emails": []
},
"sandbox": {
"name": "Sandboxes",
"owner_emails": [],
"reader_emails": []
}
} | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [custom\_role\_ids](#output\_custom\_role\_ids) | Map of custom role names to their role IDs |
+| [folder\_container\_ids](#output\_folder\_container\_ids) | Map of all folder keys to their container IDs for easy reference |
+| [organization\_role\_assignments](#output\_organization\_role\_assignments) | Map of organization-level role assignment subjects grouped by role |
+
diff --git a/src/modules/governance/README.md b/src/modules/governance/README.md
index 197dc54..dc7a7cf 100644
--- a/src/modules/governance/README.md
+++ b/src/modules/governance/README.md
@@ -1,3 +1,9 @@
+# governance
+
+[English](README.md) | [Deutsch](README.de.md)
+
+The generated technical reference below documents this module.
+
## Requirements
diff --git a/src/modules/landing-zone/README.de.md b/src/modules/landing-zone/README.de.md
new file mode 100644
index 0000000..bbf2462
--- /dev/null
+++ b/src/modules/landing-zone/README.de.md
@@ -0,0 +1,96 @@
+# landing-zone
+
+[English](README.md) | [Deutsch](README.de.md)
+
+Öffentliche Projekte können mit `network_enabled = true` ein lokales Projektnetzwerk aktivieren.
+Corporate-Projekte behalten ihr geroutetes Netzwerk und ihre SNA-Integration. Wird die
+Option weggelassen, bleiben bestehende öffentliche Projekte ohne Netzwerk unverändert.
+Das optionale `network_prefix_length` gilt für beide Arten von Projektnetzwerken.
+
+Der folgende generatorverwaltete technische Block bleibt unverändert auf Englisch;
+maßgeblich ist die [kanonische englische Referenz](README.de.md).
+
+
+## Requirements
+
+| Name | Version |
+| ---- | ------- |
+| [terraform](#requirement\_terraform) | >= 1.11 |
+| [stackit](#requirement\_stackit) | >= 0.114.0 |
+| [time](#requirement\_time) | >= 0.14.1 |
+
+## Providers
+
+| Name | Version |
+| ---- | ------- |
+| [stackit](#provider\_stackit) | >= 0.114.0 |
+| [time](#provider\_time) | >= 0.14.1 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+| ---- | ---- |
+| [stackit_authorization_project_custom_role.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_project_custom_role) | resource |
+| [stackit_authorization_project_role_assignment.sa_owner](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_project_role_assignment) | resource |
+| [stackit_authorization_project_role_assignment.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_project_role_assignment) | resource |
+| [stackit_dns_zone.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/dns_zone) | resource |
+| [stackit_network.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/network) | resource |
+| [stackit_objectstorage_bucket.default](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_bucket) | resource |
+| [stackit_objectstorage_bucket.tfstate](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_bucket) | resource |
+| [stackit_objectstorage_credential.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_credential) | resource |
+| [stackit_objectstorage_credentials_group.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_credentials_group) | resource |
+| [stackit_observability_instance.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/observability_instance) | resource |
+| [stackit_resourcemanager_project.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/resourcemanager_project) | resource |
+| [stackit_routing_table.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/routing_table) | resource |
+| [stackit_routing_table_route.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/routing_table_route) | resource |
+| [stackit_secretsmanager_instance.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/secretsmanager_instance) | resource |
+| [stackit_service_account.automation](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/service_account) | resource |
+| [stackit_service_account_key.automation](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/service_account_key) | resource |
+| [time_rotating.key_rotate](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/rotating) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+| ---- | ----------- | ---- | ------- | :------: |
+| [corporate](#input\_corporate) | Whether this landing zone uses corporate networking (network area + routing). Set to false for public internet. | `bool` | `false` | no |
+| [custom\_roles](#input\_custom\_roles) | List of custom roles to create for the project. | list(object({
name = string
description = string
permissions = list(string)
})) | n/a | yes |
+| [dns\_zone\_name](#input\_dns\_zone\_name) | Full DNS zone domain name for this landing zone. Set to null to skip DNS zone creation. | `string` | `null` | no |
+| [firewall\_next\_hop\_ip](#input\_firewall\_next\_hop\_ip) | IP address of the firewall next hop. | `string` | `null` | no |
+| [ipv4\_nameservers](#input\_ipv4\_nameservers) | List of IPv4 nameservers for the network. Null takes the network area defaults on creation and leaves an existing network untouched. | `list(string)` | `null` | no |
+| [labels](#input\_labels) | Additional labels to apply to all resources. | `map(string)` | `{}` | no |
+| [naming\_pattern](#input\_naming\_pattern) | Naming prefix for all resources in this module, e.g. "myco-pltfm-hub-prod". | `string` | n/a | yes |
+| [network\_area\_id](#input\_network\_area\_id) | Network Area ID to deploy resources into. Required if corporate is true. | `string` | `null` | no |
+| [network\_enabled](#input\_network\_enabled) | Create a local project network without SNA when corporate is false. Corporate networks are always created. | `bool` | `false` | no |
+| [network\_prefix\_length](#input\_network\_prefix\_length) | CIDR block prefix length for the project's network range. | `number` | `null` | no |
+| [observability](#input\_observability) | Optional observability instance configuration in the landing zone project. | object({
enabled = optional(bool, false)
plan_name = optional(string, "Observability-Starter-EU01")
acl = optional(list(string), [])
name = optional(string, null)
}) | `{}` | no |
+| [organization\_id](#input\_organization\_id) | Container ID of the root organization. | `string` | n/a | yes |
+| [owner\_email](#input\_owner\_email) | Email address of the project owner. Required for project creation. | `string` | n/a | yes |
+| [parent\_container\_id](#input\_parent\_container\_id) | Parent container ID (folder or organization) where the project will be created. | `string` | n/a | yes |
+| [project\_name](#input\_project\_name) | Name of the STACKIT project to create. | `string` | `null` | no |
+| [role\_assignments](#input\_role\_assignments) | List of role assignments for the project. Subject can be a user email or service account email. | list(object({
role = string
subject = string
})) | `[]` | no |
+| [secretsmanager\_acls](#input\_secretsmanager\_acls) | List of ACL rules for the Secrets Manager instance. Set to empty list for no ACLs or null to skip Secrets Manager creation. | `list(string)` | `[]` | no |
+| [secretsmanager\_enabled](#input\_secretsmanager\_enabled) | Whether to create a Secrets Manager instance in the landing zone project. | `bool` | `true` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [connected\_network\_area\_id](#output\_connected\_network\_area\_id) | The ID of the connected network area. |
+| [dns\_zone\_dns\_name](#output\_dns\_zone\_dns\_name) | The DNS name of the landing zone's child DNS zone. |
+| [dns\_zone\_id](#output\_dns\_zone\_id) | The ID of the landing zone's child DNS zone. |
+| [landing\_zone\_type](#output\_landing\_zone\_type) | The type of the landing zone, either 'corporate' or 'public'. |
+| [observability\_grafana\_admin\_password](#output\_observability\_grafana\_admin\_password) | The Grafana admin password of the optional landing zone observability instance. |
+| [observability\_grafana\_admin\_user](#output\_observability\_grafana\_admin\_user) | The Grafana admin username of the optional landing zone observability instance. |
+| [observability\_grafana\_url](#output\_observability\_grafana\_url) | The Grafana URL of the optional landing zone observability instance. |
+| [observability\_instance\_id](#output\_observability\_instance\_id) | The optional observability instance ID in the landing zone project. |
+| [observability\_metrics\_push\_url](#output\_observability\_metrics\_push\_url) | The Prometheus remote-write URL of the optional landing zone observability instance. |
+| [project\_container\_id](#output\_project\_container\_id) | The container ID of the created STACKIT project. |
+| [project\_id](#output\_project\_id) | The project ID of the created STACKIT project. |
+| [project\_name](#output\_project\_name) | The name of the created STACKIT project. |
+| [project\_network](#output\_project\_network) | Non-secret reference to the managed project network, or null when no network is created. |
+| [secretsmanager\_instance\_id](#output\_secretsmanager\_instance\_id) | The ID of the landing zone Secrets Manager instance. |
+
diff --git a/src/modules/landing-zone/README.md b/src/modules/landing-zone/README.md
index 35157a9..3aa100f 100644
--- a/src/modules/landing-zone/README.md
+++ b/src/modules/landing-zone/README.md
@@ -1,3 +1,7 @@
+# landing-zone
+
+[English](README.md) | [Deutsch](README.de.md)
+
Public projects can opt into a local project network with `network_enabled = true`.
Corporate projects retain their routed network and SNA integration. Omitting the
option preserves existing public projects without a network. The optional
diff --git a/src/modules/management/README.de.md b/src/modules/management/README.de.md
new file mode 100644
index 0000000..21c23ad
--- /dev/null
+++ b/src/modules/management/README.de.md
@@ -0,0 +1,97 @@
+# management
+
+[English](README.md) | [Deutsch](README.de.md)
+
+Die folgende generierte technische Referenz dokumentiert dieses Modul.
+Der generatorverwaltete technische Block bleibt unverändert auf Englisch;
+maßgeblich ist die [kanonische englische Referenz](README.de.md).
+
+
+## Requirements
+
+| Name | Version |
+| ---- | ------- |
+| [terraform](#requirement\_terraform) | >= 1.11 |
+| [stackit](#requirement\_stackit) | >= 0.114.0 |
+| [time](#requirement\_time) | >= 0.14.1 |
+| [vault](#requirement\_vault) | >= 5.11.0 |
+
+## Providers
+
+| Name | Version |
+| ---- | ------- |
+| [stackit](#provider\_stackit) | >= 0.114.0 |
+| [time](#provider\_time) | >= 0.14.1 |
+| [vault](#provider\_vault) | >= 5.11.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+| ---- | ---- |
+| [stackit_authorization_organization_role_assignment.sa_owner](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_organization_role_assignment) | resource |
+| [stackit_authorization_project_role_assignment.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_project_role_assignment) | resource |
+| [stackit_logs_access_token.audit_read](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/logs_access_token) | resource |
+| [stackit_logs_access_token.audit_write](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/logs_access_token) | resource |
+| [stackit_logs_instance.audit](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/logs_instance) | resource |
+| [stackit_objectstorage_bucket.audit_logs](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_bucket) | resource |
+| [stackit_objectstorage_bucket.default](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_bucket) | resource |
+| [stackit_objectstorage_bucket.tfstate](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_bucket) | resource |
+| [stackit_objectstorage_compliance_lock.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_compliance_lock) | resource |
+| [stackit_objectstorage_credential.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_credential) | resource |
+| [stackit_objectstorage_credentials_group.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_credentials_group) | resource |
+| [stackit_objectstorage_default_retention.audit_logs](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/objectstorage_default_retention) | resource |
+| [stackit_observability_credential.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/observability_credential) | resource |
+| [stackit_observability_instance.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/observability_instance) | resource |
+| [stackit_resourcemanager_project.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/resourcemanager_project) | resource |
+| [stackit_secretsmanager_instance.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/secretsmanager_instance) | resource |
+| [stackit_secretsmanager_user.default](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/secretsmanager_user) | resource |
+| [stackit_service_account.automation](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/service_account) | resource |
+| [stackit_service_account_federated_identity_provider.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/service_account_federated_identity_provider) | resource |
+| [stackit_service_account_key.automation](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/service_account_key) | resource |
+| [stackit_telemetrylink.audit](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/telemetrylink) | resource |
+| [stackit_telemetryrouter_access_token.audit_link](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/telemetryrouter_access_token) | resource |
+| [stackit_telemetryrouter_destination.audit_archive](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/telemetryrouter_destination) | resource |
+| [stackit_telemetryrouter_destination.audit_logs](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/telemetryrouter_destination) | resource |
+| [stackit_telemetryrouter_instance.audit](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/telemetryrouter_instance) | resource |
+| [time_rotating.automation](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/rotating) | resource |
+| [vault_kv_secret_v2.audit_logs](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/resources/kv_secret_v2) | resource |
+| [vault_kv_secret_v2.object_storage_credentials](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/resources/kv_secret_v2) | resource |
+| [vault_kv_secret_v2.observability](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/resources/kv_secret_v2) | resource |
+| [vault_kv_secret_v2.service_account_key_automation](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/resources/kv_secret_v2) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+| ---- | ----------- | ---- | ------- | :------: |
+| [audit\_logs](#input\_audit\_logs) | Audit logs configuration. The router forwards to two destinations: OTLP into the Logs instance for querying, and S3 into the audit bucket for long-term archive. retention\_days applies to both, driving the Logs instance retention and, when s3\_object\_lock is enabled, the archive bucket's default retention. link\_scopes defaults to a single organization-wide link; set it to attach individual folders or projects instead. | object({
retention_days = optional(number, 180)
acl = optional(list(string), null)
s3_object_lock = optional(bool, true)
link_scopes = optional(list(object({
resource_type = string # organization, folder, project
resource_id = string
})), null)
}) | `null` | no |
+| [federated\_identity\_providers](#input\_federated\_identity\_providers) | List of federated identity providers to configure for the management service account. | list(object({
name = string
issuer = string
assertions = list(object({
item = string
operator = string
value = string
}))
})) | `[]` | no |
+| [labels](#input\_labels) | Additional labels to apply to all folders. | `map(string)` | `{}` | no |
+| [naming\_pattern](#input\_naming\_pattern) | Naming prefix for all resources in this module, e.g. "myco-pltfm-hub-prod". | `string` | n/a | yes |
+| [observability](#input\_observability) | Observability instance configuration. Set to null to skip observability deployment. | object({
plan_name = optional(string, "Observability-Starter-EU01")
acl = optional(list(string), [])
logs_retention_days = optional(number, 30)
traces_retention_days = optional(number, 30)
metrics_retention_days = optional(number, 90)
metrics_retention_days_5m_downsampling = optional(number, 90)
metrics_retention_days_1h_downsampling = optional(number, 90)
}) | `null` | no |
+| [organization\_id](#input\_organization\_id) | Container ID of the root folder or organization under which the company folder will be created. | `string` | n/a | yes |
+| [owner\_email](#input\_owner\_email) | Email address of the owner for the folders. Required for STACKIT resource manager. | `string` | n/a | yes |
+| [parent\_container\_id](#input\_parent\_container\_id) | Parent container ID (folder or organization) where the project will be created. | `string` | n/a | yes |
+| [project\_name](#input\_project\_name) | Name of the STACKIT project to create. | `string` | `null` | no |
+| [role\_assignments](#input\_role\_assignments) | List of role assignments for the project. Subject can be a user email or service account email. | list(object({
role = string
subject = string
})) | `[]` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [audit\_logs\_bucket\_name](#output\_audit\_logs\_bucket\_name) | The name of the object storage bucket archiving audit logs. |
+| [audit\_logs\_datasource\_url](#output\_audit\_logs\_datasource\_url) | Datasource URL of the audit Logs instance, usable as a Grafana datasource. |
+| [audit\_logs\_instance\_id](#output\_audit\_logs\_instance\_id) | The ID of the Logs instance receiving audit logs. |
+| [audit\_telemetry\_router\_id](#output\_audit\_telemetry\_router\_id) | The ID of the Telemetry Router collecting audit logs. |
+| [bucket\_name\_tfstate](#output\_bucket\_name\_tfstate) | The name of the tfstate object storage bucket. |
+| [project\_container\_id](#output\_project\_container\_id) | The container ID of the created STACKIT project. |
+| [project\_id](#output\_project\_id) | The project ID of the created STACKIT project. |
+| [project\_name](#output\_project\_name) | The name of the created STACKIT project. |
+| [secretsmanager\_instance\_id](#output\_secretsmanager\_instance\_id) | The ID of the Secrets Manager instance, usable as the vault mount. |
+| [secretsmanager\_password](#output\_secretsmanager\_password) | The password of the default Secrets Manager user. |
+| [secretsmanager\_username](#output\_secretsmanager\_username) | The username of the default Secrets Manager user. |
+| [service\_account\_email](#output\_service\_account\_email) | The email of the created service account. |
+
diff --git a/src/modules/management/README.md b/src/modules/management/README.md
index c923065..c5db900 100644
--- a/src/modules/management/README.md
+++ b/src/modules/management/README.md
@@ -1,3 +1,9 @@
+# management
+
+[English](README.md) | [Deutsch](README.de.md)
+
+The generated technical reference below documents this module.
+
## Requirements
diff --git a/src/modules/namespace-service-demo/README.de.md b/src/modules/namespace-service-demo/README.de.md
new file mode 100644
index 0000000..c6b4e9a
--- /dev/null
+++ b/src/modules/namespace-service-demo/README.de.md
@@ -0,0 +1,54 @@
+# namespace-service-demo
+
+[English](README.md) | [Deutsch](README.de.md)
+
+Die folgende generierte technische Referenz dokumentiert dieses Modul.
+Der generatorverwaltete technische Block bleibt unverändert auf Englisch;
+maßgeblich ist die [kanonische englische Referenz](README.de.md).
+
+
+## Requirements
+
+| Name | Version |
+| ---- | ------- |
+| [terraform](#requirement\_terraform) | >= 1.11 |
+| [grafana](#requirement\_grafana) | >= 4.45.2 |
+| [stackit](#requirement\_stackit) | >= 0.114.0 |
+
+## Providers
+
+| Name | Version |
+| ---- | ------- |
+| [grafana.observability](#provider\_grafana.observability) | >= 4.45.2 |
+| [stackit](#provider\_stackit) | >= 0.114.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+| ---- | ---- |
+| [grafana_dashboard.namespace_overview](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/dashboard) | resource |
+| [grafana_data_source.platform_prometheus](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source) | resource |
+| [grafana_folder.stackit_managed](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/folder) | resource |
+| [stackit_observability_credential.platform_metrics_reader](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/observability_credential) | resource |
+| [stackit_observability_scrapeconfig.namespace_demo_ingestion](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/observability_scrapeconfig) | resource |
+| [stackit_secretsmanager_user.external_secret_demo](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/secretsmanager_user) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+| ---- | ----------- | ---- | ------- | :------: |
+| [dashboard\_folder\_title](#input\_dashboard\_folder\_title) | Folder title used for managed landing-zone demo dashboards. | `string` | `"STACKIT Managed Dashboards"` | no |
+| [dashboard\_passwords](#input\_dashboard\_passwords) | Grafana admin passwords keyed by landing-zone key for dashboard demo imports. | `map(string)` | `{}` | no |
+| [services](#input\_services) | Enabled namespace-service demo configurations keyed by landing-zone key. | map(object({
namespace = string
use_secretsmanager = bool
landing_zone_project_id = string
secretsmanager_instance_id = string
observability_instance_id = optional(string)
observability_grafana_url = optional(string)
observability_admin_user = optional(string)
demo_metrics_ingestion_enabled = optional(bool, false)
demo_metrics_ingestion_target_urls = optional(list(string), [])
demo_metrics_ingestion_scheme = optional(string, "https")
demo_metrics_ingestion_metrics_path = optional(string, "/")
demo_metrics_ingestion_scrape_interval = optional(string, "60s")
demo_metrics_ingestion_scrape_timeout = optional(string, "30s")
platform_project_id = optional(string)
platform_observability_instance_id = optional(string)
platform_observability_targets_url = optional(string)
dns_zone_name = optional(string)
})) | `{}` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [samples](#output\_samples) | Demo sample references for enabled namespace-service demos. |
+| [secret\_access](#output\_secret\_access) | Credentials for demo Secrets Manager users keyed by landing zone. |
+
diff --git a/src/modules/namespace-service-demo/README.md b/src/modules/namespace-service-demo/README.md
index 276c294..ab885c7 100644
--- a/src/modules/namespace-service-demo/README.md
+++ b/src/modules/namespace-service-demo/README.md
@@ -1,3 +1,9 @@
+# namespace-service-demo
+
+[English](README.md) | [Deutsch](README.de.md)
+
+The generated technical reference below documents this module.
+
## Requirements
diff --git a/src/modules/platform-kubernetes/README.de.md b/src/modules/platform-kubernetes/README.de.md
new file mode 100644
index 0000000..2dbbca9
--- /dev/null
+++ b/src/modules/platform-kubernetes/README.de.md
@@ -0,0 +1,86 @@
+# platform-kubernetes
+
+[English](README.md) | [Deutsch](README.de.md)
+
+Die folgende generierte technische Referenz dokumentiert dieses Modul.
+Der generatorverwaltete technische Block bleibt unverändert auf Englisch;
+maßgeblich ist die [kanonische englische Referenz](README.de.md).
+
+
+## Requirements
+
+| Name | Version |
+| ---- | ------- |
+| [terraform](#requirement\_terraform) | >= 1.11 |
+| [stackit](#requirement\_stackit) | >= 0.114.0 |
+| [time](#requirement\_time) | >= 0.14.1 |
+
+## Providers
+
+| Name | Version |
+| ---- | ------- |
+| [stackit](#provider\_stackit) | >= 0.114.0 |
+| [time](#provider\_time) | >= 0.14.1 |
+
+## Modules
+
+| Name | Source | Version |
+| ---- | ------ | ------- |
+| [debug\_bastion](#module\_debug\_bastion) | ../debug-bastion | n/a |
+
+## Resources
+
+| Name | Type |
+| ---- | ---- |
+| [stackit_authorization_project_role_assignment.kms_admin](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_project_role_assignment) | resource |
+| [stackit_authorization_project_role_assignment.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_project_role_assignment) | resource |
+| [stackit_authorization_service_account_role_assignment.ske_impersonation](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_service_account_role_assignment) | resource |
+| [stackit_dns_zone.ske_extension](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/dns_zone) | resource |
+| [stackit_kms_key.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/kms_key) | resource |
+| [stackit_kms_keyring.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/kms_keyring) | resource |
+| [stackit_network.sna](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/network) | resource |
+| [stackit_observability_instance.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/observability_instance) | resource |
+| [stackit_resourcemanager_project.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/resourcemanager_project) | resource |
+| [stackit_routing_table.sna_egress](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/routing_table) | resource |
+| [stackit_routing_table_route.sna_default_route](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/routing_table_route) | resource |
+| [stackit_service_account.kms_manager](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/service_account) | resource |
+| [stackit_ske_cluster.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/ske_cluster) | resource |
+| [stackit_ske_kubeconfig.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/ske_kubeconfig) | resource |
+| [time_sleep.wait_for_network_area_membership](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource |
+| [stackit_service_accounts.ske_internal](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/data-sources/service_accounts) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+| ---- | ----------- | ---- | ------- | :------: |
+| [cluster](#input\_cluster) | SKE cluster configuration. | object({
name = string
kubernetes_version_min = optional(string, null)
node_pools = optional(list(object({
name = string
machine_type = string
minimum = number
maximum = number
availability_zones = list(string)
allow_system_components = optional(bool, false)
volume_size = optional(number, 20)
volume_type = optional(string, "storage_premium_perf1")
os_name = optional(string, "flatcar")
labels = optional(map(string), {})
})), [
{
name = "system"
machine_type = "g3i.4"
minimum = 2
maximum = 2
availability_zones = ["eu01-1"]
allow_system_components = true
volume_size = 20
volume_type = "storage_premium_perf1"
os_name = "flatcar"
labels = {
"workload-role" = "system"
}
},
{
name = "application"
machine_type = "g3i.4"
minimum = 2
maximum = 2
availability_zones = ["eu01-2"]
allow_system_components = false
volume_size = 20
volume_type = "storage_premium_perf1"
os_name = "flatcar"
labels = {
"workload-role" = "application"
}
}
])
maintenance = optional(object({
enable_kubernetes_version_updates = optional(bool, true)
enable_machine_image_version_updates = optional(bool, true)
start = optional(string, "01:00:00Z")
end = optional(string, "02:00:00Z")
}), {})
}) | n/a | yes |
+| [debug\_bastion](#input\_debug\_bastion) | Optional debug bastion VM in the SNA network with SSH access to test SKE connectivity from inside the private network. | object({
enabled = optional(bool, false)
name = optional(string, null)
availability_zone = optional(string, null)
machine_type = optional(string, "g2i.1")
image_id = optional(string, "7b10e105-295b-4369-b6e0-567ec940a02b")
boot_volume_size = optional(number, 20)
ssh_public_key = optional(string, null)
ssh_public_key_path = optional(string, "~/.ssh/id_rsa.pub")
ssh_allowed_cidrs = optional(list(string), ["0.0.0.0/0"])
assign_public_ip = optional(bool, true)
install_kubectl = optional(bool, true)
}) | `{}` | no |
+| [dns](#input\_dns) | SKE DNS extension configuration. If create\_zones is true, zones are created in the platform project before cluster creation. gateway\_api enables Gateway API support for ExternalDNS. | object({
enabled = optional(bool, true)
create_zones = optional(bool, true)
zones = optional(list(string), [])
gateway_api = optional(bool, true)
}) | `{}` | no |
+| [encrypted\_volumes](#input\_encrypted\_volumes) | Optional encrypted volume setup for SKE via KMS and Kubernetes storage class. | object({
enabled = optional(bool, false)
storage_class_name = optional(string, "stackit-encrypted-premium")
kms_keyring_name = optional(string, "ske-volume-keyring")
kms_key_name = optional(string, "ske-volume-key")
kms_key_version = optional(string, "1")
}) | `{}` | no |
+| [labels](#input\_labels) | Additional labels to apply to resources in this module. | `map(string)` | `{}` | no |
+| [naming\_pattern](#input\_naming\_pattern) | Naming prefix for resources in this module, e.g. myco-pltfm-k8s-eu01. | `string` | n/a | yes |
+| [network](#input\_network) | Network settings for SKE. Set sna\_enabled=true and provide sna\_network\_area\_id for SNA; otherwise the cluster runs in public control-plane mode. | object({
sna_enabled = optional(bool, false)
sna_network_area_id = optional(string, null)
firewall_next_hop_ip = optional(string, null)
sna_network_prefix_length = optional(number, 24)
}) | `{}` | no |
+| [observability](#input\_observability) | Observability configuration for central cluster monitoring in the same project as the cluster. | object({
enabled = optional(bool, true)
plan_name = optional(string, "Observability-Starter-EU01")
acl = optional(list(string), [])
name = optional(string, null)
}) | `{}` | no |
+| [organization\_id](#input\_organization\_id) | Organization ID used for routing table resources in network-area scope. | `string` | n/a | yes |
+| [owner\_email](#input\_owner\_email) | Email address of the project owner. Required for project creation. | `string` | n/a | yes |
+| [parent\_container\_id](#input\_parent\_container\_id) | Parent container ID (folder or organization) where the project will be created. | `string` | n/a | yes |
+| [project\_name](#input\_project\_name) | Name of the STACKIT project to create. | `string` | `null` | no |
+| [region](#input\_region) | STACKIT region for the SKE cluster. | `string` | n/a | yes |
+| [role\_assignments](#input\_role\_assignments) | List of role assignments for the project. Subject can be a user email or service account email. | list(object({
role = string
subject = string
})) | `[]` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [debug\_bastion](#output\_debug\_bastion) | Debug bastion metadata when enabled for private cluster troubleshooting. |
+| [dns\_extension\_zones](#output\_dns\_extension\_zones) | DNS zones configured for SKE DNS extension. |
+| [encrypted\_volume\_support](#output\_encrypted\_volume\_support) | Configuration values for encrypted SKE volumes when enabled. |
+| [kube\_config](#output\_kube\_config) | Kubeconfig for the created SKE cluster. |
+| [observability\_instance\_id](#output\_observability\_instance\_id) | The observability instance ID used for cluster extension. |
+| [observability\_targets\_url](#output\_observability\_targets\_url) | The Prometheus query endpoint URL of the optional platform observability instance. |
+| [project\_container\_id](#output\_project\_container\_id) | The container ID of the created STACKIT project. |
+| [project\_id](#output\_project\_id) | The project ID of the created STACKIT project. |
+| [project\_name](#output\_project\_name) | The name of the created STACKIT project. |
+| [ske\_cluster\_name](#output\_ske\_cluster\_name) | The name of the created SKE cluster. |
+| [ske\_cluster\_region](#output\_ske\_cluster\_region) | The region of the created SKE cluster. |
+
diff --git a/src/modules/platform-kubernetes/README.md b/src/modules/platform-kubernetes/README.md
index b8b49aa..b69faea 100644
--- a/src/modules/platform-kubernetes/README.md
+++ b/src/modules/platform-kubernetes/README.md
@@ -1,3 +1,9 @@
+# platform-kubernetes
+
+[English](README.md) | [Deutsch](README.de.md)
+
+The generated technical reference below documents this module.
+
## Requirements
diff --git a/src/modules/sandboxes/README.de.md b/src/modules/sandboxes/README.de.md
new file mode 100644
index 0000000..e3a2fa3
--- /dev/null
+++ b/src/modules/sandboxes/README.de.md
@@ -0,0 +1,47 @@
+# sandboxes
+
+[English](README.md) | [Deutsch](README.de.md)
+
+Die folgende generierte technische Referenz dokumentiert dieses Modul.
+Der generatorverwaltete technische Block bleibt unverändert auf Englisch;
+maßgeblich ist die [kanonische englische Referenz](README.de.md).
+
+
+## Requirements
+
+| Name | Version |
+| ---- | ------- |
+| [terraform](#requirement\_terraform) | >= 1.11 |
+| [stackit](#requirement\_stackit) | >= 0.114.0 |
+
+## Providers
+
+| Name | Version |
+| ---- | ------- |
+| [stackit](#provider\_stackit) | >= 0.114.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+| ---- | ---- |
+| [stackit_authorization_project_role_assignment.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/authorization_project_role_assignment) | resource |
+| [stackit_resourcemanager_project.this](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/resourcemanager_project) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+| ---- | ----------- | ---- | ------- | :------: |
+| [naming\_prefix](#input\_naming\_prefix) | Naming prefix for all resources in this module. | `string` | n/a | yes |
+| [parent\_container\_id](#input\_parent\_container\_id) | Parent container ID (folder or organization) where the project will be created. | `string` | n/a | yes |
+| [sandboxes](#input\_sandboxes) | List of sandboxes to create. | list(object({
project_name = string
owner_emails = optional(list(string))
project_owner_email = string
})) | `[]` | no |
+
+## Outputs
+
+| Name | Description |
+| ---- | ----------- |
+| [projects](#output\_projects) | The created STACKIT projects with their IDs, container IDs, and names. |
+
diff --git a/src/modules/sandboxes/README.md b/src/modules/sandboxes/README.md
index 55d523a..23c33f8 100644
--- a/src/modules/sandboxes/README.md
+++ b/src/modules/sandboxes/README.md
@@ -1,3 +1,9 @@
+# sandboxes
+
+[English](README.md) | [Deutsch](README.de.md)
+
+The generated technical reference below documents this module.
+
## Requirements