Skip to content

Commit 2a27080

Browse files
authored
Merge pull request #53 from stackitcloud/16-implement-auditactivity-logs
Export org wide audit logs to a Logs instance and S3 archive
2 parents 17f47d9 + fa4a87e commit 2a27080

13 files changed

Lines changed: 364 additions & 74 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 = 180
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 = 180
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

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#############
2+
## BUCKETS ##
3+
#############
4+
5+
resource "stackit_objectstorage_bucket" "default" {
6+
name = "${var.naming_pattern}-default"
7+
project_id = stackit_resourcemanager_project.this.project_id
8+
}
9+
10+
resource "stackit_objectstorage_bucket" "tfstate" {
11+
name = "${var.naming_pattern}-tfstate"
12+
project_id = stackit_resourcemanager_project.this.project_id
13+
14+
depends_on = [
15+
stackit_objectstorage_bucket.default, # "project.create_conflict","msg":"Two concurrent calls try to create the same project"}]}
16+
]
17+
}
18+
19+
resource "stackit_objectstorage_compliance_lock" "this" {
20+
count = try(var.audit_logs.s3_object_lock, false) ? 1 : 0
21+
22+
project_id = stackit_resourcemanager_project.this.project_id
23+
24+
depends_on = [
25+
stackit_objectstorage_bucket.default,
26+
stackit_objectstorage_bucket.tfstate,
27+
]
28+
}
29+
30+
resource "stackit_objectstorage_bucket" "audit_logs" {
31+
name = "${var.naming_pattern}-audit-logs"
32+
project_id = stackit_resourcemanager_project.this.project_id
33+
34+
object_lock = try(var.audit_logs.s3_object_lock, false) ? true : null
35+
36+
depends_on = [
37+
stackit_objectstorage_bucket.tfstate, # "project.create_conflict","msg":"Two concurrent calls try to create the same project"}]}
38+
stackit_objectstorage_compliance_lock.this, # object_lock requires the project lock to exist first
39+
]
40+
}
41+
42+
resource "stackit_objectstorage_default_retention" "audit_logs" {
43+
count = try(var.audit_logs.s3_object_lock, false) ? 1 : 0
44+
45+
project_id = stackit_resourcemanager_project.this.project_id
46+
bucket_name = stackit_objectstorage_bucket.audit_logs.name
47+
days = var.audit_logs.retention_days
48+
49+
# GOVERNANCE, not COMPLIANCE: objects can still be removed early by a holder of s3:BypassGovernanceRetention, which keeps the bucket and project destroyable.
50+
mode = "GOVERNANCE"
51+
}
52+
53+
#################
54+
## CREDENTIALS ##
55+
#################
56+
57+
resource "stackit_objectstorage_credentials_group" "this" {
58+
project_id = stackit_resourcemanager_project.this.project_id
59+
name = var.naming_pattern
60+
61+
depends_on = [
62+
stackit_objectstorage_bucket.default,
63+
stackit_objectstorage_bucket.tfstate,
64+
stackit_objectstorage_bucket.audit_logs
65+
]
66+
}
67+
68+
resource "stackit_objectstorage_credential" "this" {
69+
project_id = stackit_resourcemanager_project.this.project_id
70+
credentials_group_id = stackit_objectstorage_credentials_group.this.credentials_group_id
71+
}
72+
73+
resource "vault_kv_secret_v2" "object_storage_credentials" {
74+
mount = stackit_secretsmanager_instance.this.instance_id
75+
name = "object_storage_credentials_${replace(var.naming_pattern, "-", "_")}"
76+
cas = 1
77+
delete_all_versions = true
78+
data_json = jsonencode(
79+
{
80+
ACCESS_KEY = stackit_objectstorage_credential.this.access_key,
81+
SECRET_ACCESS_KEY = stackit_objectstorage_credential.this.secret_access_key
82+
}
83+
)
84+
}

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: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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 = "https://${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+
# The API returns these without a scheme; store them ready to use.
132+
INGEST_OTLP_URL = "https://${stackit_logs_instance.audit[0].ingest_otlp_url}"
133+
QUERY_URL = "https://${stackit_logs_instance.audit[0].query_url}"
134+
DATASOURCE_URL = "https://${stackit_logs_instance.audit[0].datasource_url}"
135+
WRITE_TOKEN = stackit_logs_access_token.audit_write[0].access_token
136+
READ_TOKEN = stackit_logs_access_token.audit_read[0].access_token
137+
ROUTER_TOKEN = stackit_telemetryrouter_access_token.audit_link[0].access_token
138+
139+
# Credentials for this bucket are the project ones, already in object_storage_credentials_*.
140+
ARCHIVE_BUCKET = stackit_objectstorage_bucket.audit_logs.name
141+
ARCHIVE_ENDPOINT = trimsuffix(stackit_objectstorage_bucket.audit_logs.url_path_style, "/${stackit_objectstorage_bucket.audit_logs.name}")
142+
}
143+
)
144+
}

0 commit comments

Comments
 (0)