diff --git a/src/config/hub-and-spoke-firewall.tfvars b/src/config/hub-and-spoke-firewall.tfvars index 6baf534..b89f0c9 100644 --- a/src/config/hub-and-spoke-firewall.tfvars +++ b/src/config/hub-and-spoke-firewall.tfvars @@ -35,6 +35,12 @@ labels = { # plan_name = "Observability-Starter-EU01" # } +# # Route organization-wide audit logs into a Logs instance in the management project. +# # Omit link_scopes for a single organization-wide link, or list folders/projects explicitly. +# audit_logs = { +# retention_days = 180 +# } + # # Federated identity providers for the management service account (e.g. GitHub Actions OIDC) # federated_identity_providers = [ # { diff --git a/src/config/hub-and-spoke.tfvars b/src/config/hub-and-spoke.tfvars index ed3d91b..a22e54a 100644 --- a/src/config/hub-and-spoke.tfvars +++ b/src/config/hub-and-spoke.tfvars @@ -35,6 +35,12 @@ labels = { # plan_name = "Observability-Starter-EU01" # } +# # Route organization-wide audit logs into a Logs instance in the management project. +# # Omit link_scopes for a single organization-wide link, or list folders/projects explicitly. +# audit_logs = { +# retention_days = 180 +# } + # # Federated identity providers for the management service account (e.g. GitHub Actions OIDC) # federated_identity_providers = [ # { diff --git a/src/main.tf b/src/main.tf index 71c8f47..4827c3d 100644 --- a/src/main.tf +++ b/src/main.tf @@ -28,6 +28,7 @@ module "management" { organization_id = var.organization_id labels = var.labels observability = var.observability + audit_logs = var.audit_logs federated_identity_providers = var.federated_identity_providers } diff --git a/src/modules/management/3-bucket.tf b/src/modules/management/3-bucket.tf deleted file mode 100644 index c0a731e..0000000 --- a/src/modules/management/3-bucket.tf +++ /dev/null @@ -1,45 +0,0 @@ -#################### -## OBJECT STORAGE ## -#################### - -resource "stackit_objectstorage_bucket" "default" { - name = "${var.naming_pattern}-default" - project_id = stackit_resourcemanager_project.this.project_id -} - -resource "stackit_objectstorage_bucket" "tfstate" { - name = "${var.naming_pattern}-tfstate" - project_id = stackit_resourcemanager_project.this.project_id - - depends_on = [ - stackit_objectstorage_bucket.default, # "project.create_conflict","msg":"Two concurrent calls try to create the same project"}]} - ] -} - -resource "stackit_objectstorage_credentials_group" "this" { - project_id = stackit_resourcemanager_project.this.project_id - name = var.naming_pattern - - depends_on = [ - stackit_objectstorage_bucket.default, - stackit_objectstorage_bucket.tfstate - ] -} - -resource "stackit_objectstorage_credential" "this" { - project_id = stackit_resourcemanager_project.this.project_id - credentials_group_id = stackit_objectstorage_credentials_group.this.credentials_group_id -} - -resource "vault_kv_secret_v2" "object_storage_credentials" { - mount = stackit_secretsmanager_instance.this.instance_id - name = "object_storage_credentials_${replace(var.naming_pattern, "-", "_")}" - cas = 1 - delete_all_versions = true - data_json = jsonencode( - { - ACCESS_KEY = stackit_objectstorage_credential.this.access_key, - SECRET_ACCESS_KEY = stackit_objectstorage_credential.this.secret_access_key - } - ) -} \ No newline at end of file diff --git a/src/modules/management/3-object-storage.tf b/src/modules/management/3-object-storage.tf new file mode 100644 index 0000000..305718c --- /dev/null +++ b/src/modules/management/3-object-storage.tf @@ -0,0 +1,84 @@ +############# +## BUCKETS ## +############# + +resource "stackit_objectstorage_bucket" "default" { + name = "${var.naming_pattern}-default" + project_id = stackit_resourcemanager_project.this.project_id +} + +resource "stackit_objectstorage_bucket" "tfstate" { + name = "${var.naming_pattern}-tfstate" + project_id = stackit_resourcemanager_project.this.project_id + + depends_on = [ + stackit_objectstorage_bucket.default, # "project.create_conflict","msg":"Two concurrent calls try to create the same project"}]} + ] +} + +resource "stackit_objectstorage_compliance_lock" "this" { + count = try(var.audit_logs.s3_object_lock, false) ? 1 : 0 + + project_id = stackit_resourcemanager_project.this.project_id + + depends_on = [ + stackit_objectstorage_bucket.default, + stackit_objectstorage_bucket.tfstate, + ] +} + +resource "stackit_objectstorage_bucket" "audit_logs" { + name = "${var.naming_pattern}-audit-logs" + project_id = stackit_resourcemanager_project.this.project_id + + object_lock = try(var.audit_logs.s3_object_lock, false) ? true : null + + depends_on = [ + stackit_objectstorage_bucket.tfstate, # "project.create_conflict","msg":"Two concurrent calls try to create the same project"}]} + stackit_objectstorage_compliance_lock.this, # object_lock requires the project lock to exist first + ] +} + +resource "stackit_objectstorage_default_retention" "audit_logs" { + count = try(var.audit_logs.s3_object_lock, false) ? 1 : 0 + + project_id = stackit_resourcemanager_project.this.project_id + bucket_name = stackit_objectstorage_bucket.audit_logs.name + days = var.audit_logs.retention_days + + # GOVERNANCE, not COMPLIANCE: objects can still be removed early by a holder of s3:BypassGovernanceRetention, which keeps the bucket and project destroyable. + mode = "GOVERNANCE" +} + +################# +## CREDENTIALS ## +################# + +resource "stackit_objectstorage_credentials_group" "this" { + project_id = stackit_resourcemanager_project.this.project_id + name = var.naming_pattern + + depends_on = [ + stackit_objectstorage_bucket.default, + stackit_objectstorage_bucket.tfstate, + stackit_objectstorage_bucket.audit_logs + ] +} + +resource "stackit_objectstorage_credential" "this" { + project_id = stackit_resourcemanager_project.this.project_id + credentials_group_id = stackit_objectstorage_credentials_group.this.credentials_group_id +} + +resource "vault_kv_secret_v2" "object_storage_credentials" { + mount = stackit_secretsmanager_instance.this.instance_id + name = "object_storage_credentials_${replace(var.naming_pattern, "-", "_")}" + cas = 1 + delete_all_versions = true + data_json = jsonencode( + { + ACCESS_KEY = stackit_objectstorage_credential.this.access_key, + SECRET_ACCESS_KEY = stackit_objectstorage_credential.this.secret_access_key + } + ) +} \ No newline at end of file diff --git a/src/modules/management/4-service-account.tf b/src/modules/management/4-service-account.tf index 4652731..01783e0 100644 --- a/src/modules/management/4-service-account.tf +++ b/src/modules/management/4-service-account.tf @@ -7,7 +7,7 @@ resource "stackit_service_account" "automation" { name = substr(replace("${var.naming_pattern}-automation", "-", ""), 0, 20) } -resource "time_rotating" "key_rotate" { +resource "time_rotating" "automation" { rotation_days = 60 } @@ -17,7 +17,7 @@ resource "stackit_service_account_key" "automation" { ttl_days = 90 rotate_when_changed = { - rotation = time_rotating.key_rotate.id + rotation = time_rotating.automation.id } } diff --git a/src/modules/management/6-audit-logs.tf b/src/modules/management/6-audit-logs.tf new file mode 100644 index 0000000..f2c94ed --- /dev/null +++ b/src/modules/management/6-audit-logs.tf @@ -0,0 +1,144 @@ +################### +## LOGS INSTANCE ## +################### + +resource "stackit_logs_instance" "audit" { + count = var.audit_logs != null ? 1 : 0 + + project_id = stackit_resourcemanager_project.this.project_id + display_name = "${var.naming_pattern}-audit" + retention_days = var.audit_logs.retention_days + acl = var.audit_logs.acl + description = "Audit log sink for ${var.naming_pattern}" +} + +resource "stackit_logs_access_token" "audit_write" { + count = var.audit_logs != null ? 1 : 0 + + project_id = stackit_resourcemanager_project.this.project_id + instance_id = stackit_logs_instance.audit[0].instance_id + display_name = "${var.naming_pattern}-audit-write" + permissions = ["write"] + description = "Bearer token the telemetry router uses to ingest audit logs" +} + +resource "stackit_logs_access_token" "audit_read" { + count = var.audit_logs != null ? 1 : 0 + + project_id = stackit_resourcemanager_project.this.project_id + instance_id = stackit_logs_instance.audit[0].instance_id + display_name = "${var.naming_pattern}-audit-read" + permissions = ["read"] + description = "Read token for querying audit logs, e.g. as a Grafana datasource" +} + +###################### +## TELEMETRY ROUTER ## +###################### + +resource "stackit_telemetryrouter_instance" "audit" { + count = var.audit_logs != null ? 1 : 0 + + project_id = stackit_resourcemanager_project.this.project_id + display_name = "${var.naming_pattern}-audit" + description = "Central ingestion point for STACKIT audit logs" +} + +resource "stackit_telemetryrouter_destination" "audit_logs" { + count = var.audit_logs != null ? 1 : 0 + + project_id = stackit_resourcemanager_project.this.project_id + instance_id = stackit_telemetryrouter_instance.audit[0].instance_id + display_name = "${var.naming_pattern}-audit-logs" + description = "Forwards the audit log stream into the Logs instance" + + config = { + config_type = "OpenTelemetry" + opentelemetry = { + uri = "https://${stackit_logs_instance.audit[0].ingest_otlp_url}" + bearer_token = stackit_logs_access_token.audit_write[0].access_token + } + } +} + +resource "stackit_telemetryrouter_destination" "audit_archive" { + count = var.audit_logs != null ? 1 : 0 + + project_id = stackit_resourcemanager_project.this.project_id + instance_id = stackit_telemetryrouter_instance.audit[0].instance_id + display_name = "${var.naming_pattern}-audit-archive" + description = "Archives the audit log stream to object storage for long-term retention" + + config = { + config_type = "S3" + s3 = { + bucket = stackit_objectstorage_bucket.audit_logs.name + endpoint = trimsuffix(stackit_objectstorage_bucket.audit_logs.url_path_style, "/${stackit_objectstorage_bucket.audit_logs.name}") + access_key = { + id = stackit_objectstorage_credential.this.access_key + secret = stackit_objectstorage_credential.this.secret_access_key + } + } + } +} + +resource "stackit_telemetryrouter_access_token" "audit_link" { + count = var.audit_logs != null ? 1 : 0 + + project_id = stackit_resourcemanager_project.this.project_id + instance_id = stackit_telemetryrouter_instance.audit[0].instance_id + display_name = "${var.naming_pattern}-audit-link" + description = "Used by telemetry links to push audit logs into the router" +} + +#################### +## TELEMETRY LINK ## +#################### + +locals { + audit_log_link_scopes = var.audit_logs != null ? coalesce( + var.audit_logs.link_scopes, + [{ resource_type = "organization", resource_id = var.organization_id }] + ) : [] +} + +# link(org/folder/project) --> telemetry router --+--> OTLP --> logs instance (query) +# `--> S3 --> audit bucket (archive) +resource "stackit_telemetrylink" "audit" { + for_each = { for scope in local.audit_log_link_scopes : "${scope.resource_type}-${scope.resource_id}" => scope } + + resource_type = each.value.resource_type + resource_id = each.value.resource_id + display_name = "${var.naming_pattern}-audit" + description = "Streams ${each.value.resource_type} audit logs to the platform telemetry router" + telemetry_router_id = stackit_telemetryrouter_instance.audit[0].instance_id + access_token = stackit_telemetryrouter_access_token.audit_link[0].access_token +} + +############ +## SECRET ## +############ + +resource "vault_kv_secret_v2" "audit_logs" { + count = var.audit_logs != null ? 1 : 0 + + mount = stackit_secretsmanager_instance.this.instance_id + name = "audit_logs_${replace(var.naming_pattern, "-", "_")}" + cas = 1 + delete_all_versions = true + data_json = jsonencode( + { + # The API returns these without a scheme; store them ready to use. + INGEST_OTLP_URL = "https://${stackit_logs_instance.audit[0].ingest_otlp_url}" + QUERY_URL = "https://${stackit_logs_instance.audit[0].query_url}" + DATASOURCE_URL = "https://${stackit_logs_instance.audit[0].datasource_url}" + WRITE_TOKEN = stackit_logs_access_token.audit_write[0].access_token + READ_TOKEN = stackit_logs_access_token.audit_read[0].access_token + ROUTER_TOKEN = stackit_telemetryrouter_access_token.audit_link[0].access_token + + # Credentials for this bucket are the project ones, already in object_storage_credentials_*. + ARCHIVE_BUCKET = stackit_objectstorage_bucket.audit_logs.name + ARCHIVE_ENDPOINT = trimsuffix(stackit_objectstorage_bucket.audit_logs.url_path_style, "/${stackit_objectstorage_bucket.audit_logs.name}") + } + ) +} diff --git a/src/modules/management/README.md b/src/modules/management/README.md index b5c9b49..8d84a8c 100644 --- a/src/modules/management/README.md +++ b/src/modules/management/README.md @@ -2,16 +2,16 @@ ## Requirements | Name | Version | -|------|---------| +| ---- | ------- | | [terraform](#requirement\_terraform) | >= 1.10 | -| [stackit](#requirement\_stackit) | >=0.93.0 | -| [time](#requirement\_time) | >=0.13.1 | -| [vault](#requirement\_vault) | >=5.7.0 | +| [stackit](#requirement\_stackit) | 0.106.0 | +| [time](#requirement\_time) | ~> 0.14.0 | +| [vault](#requirement\_vault) | 5.10.1 | ## Providers | Name | Version | -|------|---------| +| ---- | ------- | | [stackit](#provider\_stackit) | 0.93.0 | | [time](#provider\_time) | 0.13.1 | | [vault](#provider\_vault) | 5.9.0 | @@ -23,30 +23,43 @@ 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_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_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 | -| [time_rotating.key_rotate](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/rotating) | 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 | +| ---- | ---- | +| [stackit_authorization_organization_role_assignment.sa_owner](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/authorization_organization_role_assignment) | resource | +| [stackit_authorization_project_role_assignment.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/authorization_project_role_assignment) | resource | +| [stackit_logs_access_token.audit_read](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/logs_access_token) | resource | +| [stackit_logs_access_token.audit_write](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/logs_access_token) | resource | +| [stackit_logs_instance.audit](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/logs_instance) | resource | +| [stackit_objectstorage_bucket.audit_logs](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/objectstorage_bucket) | resource | +| [stackit_objectstorage_bucket.default](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/objectstorage_bucket) | resource | +| [stackit_objectstorage_bucket.tfstate](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/objectstorage_bucket) | resource | +| [stackit_objectstorage_compliance_lock.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/objectstorage_compliance_lock) | resource | +| [stackit_objectstorage_credential.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/objectstorage_credential) | resource | +| [stackit_objectstorage_credentials_group.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/objectstorage_credentials_group) | resource | +| [stackit_objectstorage_default_retention.audit_logs](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/objectstorage_default_retention) | resource | +| [stackit_observability_credential.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/observability_credential) | resource | +| [stackit_observability_instance.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/observability_instance) | resource | +| [stackit_resourcemanager_project.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/resourcemanager_project) | resource | +| [stackit_secretsmanager_instance.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/secretsmanager_instance) | resource | +| [stackit_secretsmanager_user.default](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/secretsmanager_user) | resource | +| [stackit_service_account.automation](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/service_account) | resource | +| [stackit_service_account_federated_identity_provider.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/service_account_federated_identity_provider) | resource | +| [stackit_service_account_key.automation](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/service_account_key) | resource | +| [stackit_telemetrylink.audit](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/telemetrylink) | resource | +| [stackit_telemetryrouter_access_token.audit_link](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/telemetryrouter_access_token) | resource | +| [stackit_telemetryrouter_destination.audit_archive](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/telemetryrouter_destination) | resource | +| [stackit_telemetryrouter_destination.audit_logs](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/docs/resources/telemetryrouter_destination) | resource | +| [stackit_telemetryrouter_instance.audit](https://registry.terraform.io/providers/stackitcloud/stackit/0.106.0/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/5.10.1/docs/resources/kv_secret_v2) | resource | +| [vault_kv_secret_v2.object_storage_credentials](https://registry.terraform.io/providers/hashicorp/vault/5.10.1/docs/resources/kv_secret_v2) | resource | +| [vault_kv_secret_v2.observability](https://registry.terraform.io/providers/hashicorp/vault/5.10.1/docs/resources/kv_secret_v2) | resource | +| [vault_kv_secret_v2.service_account_key_automation](https://registry.terraform.io/providers/hashicorp/vault/5.10.1/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, 30)
acl = optional(list(string), null)
s3_object_lock = optional(bool, false)
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 | @@ -60,11 +73,16 @@ No modules. ## 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/outputs.tf b/src/modules/management/outputs.tf index 4675743..763545a 100644 --- a/src/modules/management/outputs.tf +++ b/src/modules/management/outputs.tf @@ -37,3 +37,23 @@ output "secretsmanager_instance_id" { description = "The ID of the Secrets Manager instance, usable as the vault mount." value = stackit_secretsmanager_instance.this.instance_id } + +output "audit_logs_instance_id" { + description = "The ID of the Logs instance receiving audit logs." + value = try(stackit_logs_instance.audit[0].instance_id, null) +} + +output "audit_logs_datasource_url" { + description = "Datasource URL of the audit Logs instance, usable as a Grafana datasource." + value = try(stackit_logs_instance.audit[0].datasource_url, null) +} + +output "audit_telemetry_router_id" { + description = "The ID of the Telemetry Router collecting audit logs." + value = try(stackit_telemetryrouter_instance.audit[0].instance_id, null) +} + +output "audit_logs_bucket_name" { + description = "The name of the object storage bucket archiving audit logs." + value = stackit_objectstorage_bucket.audit_logs.name +} diff --git a/src/modules/management/variables.tf b/src/modules/management/variables.tf index a5e5ed6..9d8361f 100644 --- a/src/modules/management/variables.tf +++ b/src/modules/management/variables.tf @@ -65,4 +65,31 @@ variable "federated_identity_providers" { })) description = "List of federated identity providers to configure for the management service account." default = [] +} + +variable "audit_logs" { + type = 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) + }) + description = "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." + default = null + + validation { + condition = alltrue([ + for scope in try(var.audit_logs.link_scopes, []) == null ? [] : try(var.audit_logs.link_scopes, []) : + contains(["organization", "folder", "project"], scope.resource_type) + ]) + error_message = "audit_logs.link_scopes[*].resource_type must be one of: organization, folder, project." + } + + validation { + condition = try(!var.audit_logs.s3_object_lock || (var.audit_logs.retention_days >= 1 && var.audit_logs.retention_days <= 365), true) + error_message = "audit_logs.retention_days must be between 1 and 365 when s3_object_lock is enabled, since that is the STACKIT object storage maximum." + } } \ No newline at end of file diff --git a/src/modules/namespace-service-demo/main.tf b/src/modules/namespace-service-demo/main.tf index 14df412..9852b86 100644 --- a/src/modules/namespace-service-demo/main.tf +++ b/src/modules/namespace-service-demo/main.tf @@ -6,7 +6,7 @@ terraform { } grafana = { source = "grafana/grafana" - version = "4.41.0" + version = "4.42.0" } } } diff --git a/src/outputs.tf b/src/outputs.tf index 0355026..2d67061 100644 --- a/src/outputs.tf +++ b/src/outputs.tf @@ -22,6 +22,21 @@ output "management_bucket_name_tfstate" { value = module.management.bucket_name_tfstate } +output "audit_logs_instance_id" { + description = "The ID of the Logs instance receiving audit logs. Null when audit_logs is not configured." + value = module.management.audit_logs_instance_id +} + +output "audit_logs_datasource_url" { + description = "Datasource URL of the audit Logs instance, usable as a Grafana datasource." + value = module.management.audit_logs_datasource_url +} + +output "audit_telemetry_router_id" { + description = "The ID of the Telemetry Router collecting audit logs." + value = module.management.audit_telemetry_router_id +} + output "connectivity_network_area_id" { description = "The network area ID created by the regional module." value = try(module.connectivity[0].network_area_id, null) diff --git a/src/variables.tf b/src/variables.tf index 6359598..3af977a 100644 --- a/src/variables.tf +++ b/src/variables.tf @@ -182,6 +182,20 @@ variable "observability" { default = null } +variable "audit_logs" { + type = 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) + }) + description = "Audit log routing for the management module. A Telemetry Link streams audit events into a Telemetry Router, which forwards them to a Logs instance and archives them to object storage. link_scopes defaults to a single organization-wide link. retention_days applies to both the Logs instance and, when s3_object_lock is enabled, the archive bucket's default retention. s3_object_lock is off by default and turns on GOVERNANCE-mode WORM protection for the archive bucket, which is not cleanly reversible. Set to null to skip audit logs deployment." + default = null +} + variable "federated_identity_providers" { type = list(object({ name = string