Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/config/hub-and-spoke-firewall.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
# {
Expand Down
6 changes: 6 additions & 0 deletions src/config/hub-and-spoke.tfvars
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
# {
Expand Down
1 change: 1 addition & 0 deletions src/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
45 changes: 0 additions & 45 deletions src/modules/management/3-bucket.tf

This file was deleted.

84 changes: 84 additions & 0 deletions src/modules/management/3-object-storage.tf
Original file line number Diff line number Diff line change
@@ -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
}
)
}
4 changes: 2 additions & 2 deletions src/modules/management/4-service-account.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}
}

Expand Down
144 changes: 144 additions & 0 deletions src/modules/management/6-audit-logs.tf
Original file line number Diff line number Diff line change
@@ -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}")
}
)
}
Loading
Loading