Skip to content

Commit 1e20876

Browse files
committed
feat(audit-logs): implement organization-wide audit logs configuration and resources
1 parent 17f47d9 commit 1e20876

12 files changed

Lines changed: 318 additions & 30 deletions

File tree

src/config/hub-and-spoke-firewall.tfvars

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ labels = {
3535
# plan_name = "Observability-Starter-EU01"
3636
# }
3737

38+
# # Route organization-wide audit logs into a Logs instance in the management project.
39+
# # Omit link_scopes for a single organization-wide link, or list folders/projects explicitly.
40+
# audit_logs = {
41+
# retention_days = 30
42+
# }
43+
3844
# # Federated identity providers for the management service account (e.g. GitHub Actions OIDC)
3945
# federated_identity_providers = [
4046
# {

src/config/hub-and-spoke.tfvars

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ labels = {
3535
# plan_name = "Observability-Starter-EU01"
3636
# }
3737

38+
# # Route organization-wide audit logs into a Logs instance in the management project.
39+
# # Omit link_scopes for a single organization-wide link, or list folders/projects explicitly.
40+
# audit_logs = {
41+
# retention_days = 30
42+
# }
43+
3844
# # Federated identity providers for the management service account (e.g. GitHub Actions OIDC)
3945
# federated_identity_providers = [
4046
# {

src/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module "management" {
2828
organization_id = var.organization_id
2929
labels = var.labels
3030
observability = var.observability
31+
audit_logs = var.audit_logs
3132
federated_identity_providers = var.federated_identity_providers
3233
}
3334

src/modules/management/3-bucket.tf renamed to src/modules/management/3-object-storage.tf

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22
## OBJECT STORAGE ##
33
####################
44

5+
resource "stackit_objectstorage_compliance_lock" "this" {
6+
count = try(var.audit_logs.s3_object_lock, false) ? 1 : 0
7+
8+
project_id = stackit_resourcemanager_project.this.project_id
9+
}
10+
11+
#############
12+
## BUCKETS ##
13+
#############
14+
515
resource "stackit_objectstorage_bucket" "default" {
616
name = "${var.naming_pattern}-default"
717
project_id = stackit_resourcemanager_project.this.project_id
@@ -16,13 +26,41 @@ resource "stackit_objectstorage_bucket" "tfstate" {
1626
]
1727
}
1828

29+
resource "stackit_objectstorage_bucket" "audit_logs" {
30+
name = "${var.naming_pattern}-audit-logs"
31+
project_id = stackit_resourcemanager_project.this.project_id
32+
33+
object_lock = local.audit_object_lock ? true : null
34+
35+
depends_on = [
36+
stackit_objectstorage_bucket.tfstate, # "project.create_conflict","msg":"Two concurrent calls try to create the same project"}]}
37+
stackit_objectstorage_compliance_lock.this, # object_lock requires the project lock to exist first
38+
]
39+
}
40+
41+
resource "stackit_objectstorage_default_retention" "audit_logs" {
42+
count = local.audit_object_lock ? 1 : 0
43+
44+
project_id = stackit_resourcemanager_project.this.project_id
45+
bucket_name = stackit_objectstorage_bucket.audit_logs.name
46+
days = var.audit_logs.retention_days
47+
48+
# GOVERNANCE, not COMPLIANCE: objects can still be removed early by a holder of s3:BypassGovernanceRetention, which keeps the bucket and project destroyable.
49+
mode = "GOVERNANCE"
50+
}
51+
52+
#################
53+
## CREDENTIALS ##
54+
#################
55+
1956
resource "stackit_objectstorage_credentials_group" "this" {
2057
project_id = stackit_resourcemanager_project.this.project_id
2158
name = var.naming_pattern
2259

2360
depends_on = [
2461
stackit_objectstorage_bucket.default,
25-
stackit_objectstorage_bucket.tfstate
62+
stackit_objectstorage_bucket.tfstate,
63+
stackit_objectstorage_bucket.audit_logs
2664
]
2765
}
2866

src/modules/management/4-service-account.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ resource "stackit_service_account" "automation" {
77
name = substr(replace("${var.naming_pattern}-automation", "-", ""), 0, 20)
88
}
99

10-
resource "time_rotating" "key_rotate" {
10+
resource "time_rotating" "automation" {
1111
rotation_days = 60
1212
}
1313

@@ -17,7 +17,7 @@ resource "stackit_service_account_key" "automation" {
1717
ttl_days = 90
1818

1919
rotate_when_changed = {
20-
rotation = time_rotating.key_rotate.id
20+
rotation = time_rotating.automation.id
2121
}
2222
}
2323

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
###################
2+
## LOGS INSTANCE ##
3+
###################
4+
5+
resource "stackit_logs_instance" "audit" {
6+
count = var.audit_logs != null ? 1 : 0
7+
8+
project_id = stackit_resourcemanager_project.this.project_id
9+
display_name = "${var.naming_pattern}-audit"
10+
retention_days = var.audit_logs.retention_days
11+
acl = var.audit_logs.acl
12+
description = "Audit log sink for ${var.naming_pattern}"
13+
}
14+
15+
resource "stackit_logs_access_token" "audit_write" {
16+
count = var.audit_logs != null ? 1 : 0
17+
18+
project_id = stackit_resourcemanager_project.this.project_id
19+
instance_id = stackit_logs_instance.audit[0].instance_id
20+
display_name = "${var.naming_pattern}-audit-write"
21+
permissions = ["write"]
22+
description = "Bearer token the telemetry router uses to ingest audit logs"
23+
}
24+
25+
resource "stackit_logs_access_token" "audit_read" {
26+
count = var.audit_logs != null ? 1 : 0
27+
28+
project_id = stackit_resourcemanager_project.this.project_id
29+
instance_id = stackit_logs_instance.audit[0].instance_id
30+
display_name = "${var.naming_pattern}-audit-read"
31+
permissions = ["read"]
32+
description = "Read token for querying audit logs, e.g. as a Grafana datasource"
33+
}
34+
35+
######################
36+
## TELEMETRY ROUTER ##
37+
######################
38+
39+
resource "stackit_telemetryrouter_instance" "audit" {
40+
count = var.audit_logs != null ? 1 : 0
41+
42+
project_id = stackit_resourcemanager_project.this.project_id
43+
display_name = "${var.naming_pattern}-audit"
44+
description = "Central ingestion point for STACKIT audit logs"
45+
}
46+
47+
resource "stackit_telemetryrouter_destination" "audit_logs" {
48+
count = var.audit_logs != null ? 1 : 0
49+
50+
project_id = stackit_resourcemanager_project.this.project_id
51+
instance_id = stackit_telemetryrouter_instance.audit[0].instance_id
52+
display_name = "${var.naming_pattern}-audit-logs"
53+
description = "Forwards the audit log stream into the Logs instance"
54+
55+
config = {
56+
config_type = "OpenTelemetry"
57+
opentelemetry = {
58+
uri = stackit_logs_instance.audit[0].ingest_otlp_url
59+
bearer_token = stackit_logs_access_token.audit_write[0].access_token
60+
}
61+
}
62+
}
63+
64+
resource "stackit_telemetryrouter_destination" "audit_archive" {
65+
count = var.audit_logs != null ? 1 : 0
66+
67+
project_id = stackit_resourcemanager_project.this.project_id
68+
instance_id = stackit_telemetryrouter_instance.audit[0].instance_id
69+
display_name = "${var.naming_pattern}-audit-archive"
70+
description = "Archives the audit log stream to object storage for long-term retention"
71+
72+
config = {
73+
config_type = "S3"
74+
s3 = {
75+
bucket = stackit_objectstorage_bucket.audit_logs.name
76+
endpoint = trimsuffix(stackit_objectstorage_bucket.audit_logs.url_path_style, "/${stackit_objectstorage_bucket.audit_logs.name}")
77+
access_key = {
78+
id = stackit_objectstorage_credential.this.access_key
79+
secret = stackit_objectstorage_credential.this.secret_access_key
80+
}
81+
}
82+
}
83+
}
84+
85+
resource "stackit_telemetryrouter_access_token" "audit_link" {
86+
count = var.audit_logs != null ? 1 : 0
87+
88+
project_id = stackit_resourcemanager_project.this.project_id
89+
instance_id = stackit_telemetryrouter_instance.audit[0].instance_id
90+
display_name = "${var.naming_pattern}-audit-link"
91+
description = "Used by telemetry links to push audit logs into the router"
92+
}
93+
94+
####################
95+
## TELEMETRY LINK ##
96+
####################
97+
98+
locals {
99+
audit_log_link_scopes = var.audit_logs != null ? coalesce(
100+
var.audit_logs.link_scopes,
101+
[{ resource_type = "organization", resource_id = var.organization_id }]
102+
) : []
103+
}
104+
105+
# link(org/folder/project) --> telemetry router --+--> OTLP --> logs instance (query)
106+
# `--> S3 --> audit bucket (archive)
107+
resource "stackit_telemetrylink" "audit" {
108+
for_each = { for scope in local.audit_log_link_scopes : "${scope.resource_type}-${scope.resource_id}" => scope }
109+
110+
resource_type = each.value.resource_type
111+
resource_id = each.value.resource_id
112+
display_name = "${var.naming_pattern}-audit"
113+
description = "Streams ${each.value.resource_type} audit logs to the platform telemetry router"
114+
telemetry_router_id = stackit_telemetryrouter_instance.audit[0].instance_id
115+
access_token = stackit_telemetryrouter_access_token.audit_link[0].access_token
116+
}
117+
118+
############
119+
## SECRET ##
120+
############
121+
122+
resource "vault_kv_secret_v2" "audit_logs" {
123+
count = var.audit_logs != null ? 1 : 0
124+
125+
mount = stackit_secretsmanager_instance.this.instance_id
126+
name = "audit_logs_${replace(var.naming_pattern, "-", "_")}"
127+
cas = 1
128+
delete_all_versions = true
129+
data_json = jsonencode(
130+
{
131+
INGEST_OTLP_URL = stackit_logs_instance.audit[0].ingest_otlp_url
132+
QUERY_URL = stackit_logs_instance.audit[0].query_url
133+
DATASOURCE_URL = stackit_logs_instance.audit[0].datasource_url
134+
WRITE_TOKEN = stackit_logs_access_token.audit_write[0].access_token
135+
READ_TOKEN = stackit_logs_access_token.audit_read[0].access_token
136+
ROUTER_TOKEN = stackit_telemetryrouter_access_token.audit_link[0].access_token
137+
138+
# Credentials for this bucket are the project ones, already in object_storage_credentials_*.
139+
ARCHIVE_BUCKET = stackit_objectstorage_bucket.audit_logs.name
140+
ARCHIVE_ENDPOINT = trimsuffix(stackit_objectstorage_bucket.audit_logs.url_path_style, "/${stackit_objectstorage_bucket.audit_logs.name}")
141+
}
142+
)
143+
}

0 commit comments

Comments
 (0)