Files
alva/infra/terraform/20-databas.tf
T
Claude cbc7bf2751 Genomgång av flöden och infrastruktur — infrakarta i Terraform
Infrastrukturen får en definition som går att läsa: infra/terraform, där
karta.tf beskriver hela systemet en gång som data — tjänster, portar,
routing, vilken tjänst som ser vilken hemlighet, dataflöden och gränser.
Resten av filerna läser därifrån i stället för att upprepa namn och
portar, och `terraform output karta` skriver ut samma innehåll i klartext
direkt ur definitionen. Filerna är numrerade i läsordning.

Genomgången hittade sex saker som är åtgärdade här:

Uppslaget mot märkesspecifika kopplingar kunde riktas inåt. Bas-URL:en
sätts av kundens administratör men anropet görs av vår server —
169.254.169.254 eller ett internt tjänstenamn hade nått molnets
metadatatjänst respektive klustrets insida, och svaret kommit tillbaka
mappat genom svarsfälten. En tenant-administratör är inte
infrastrukturens ägare. Nu stoppas IP-literaler, namn som resolvar till
privata adresser och .local/.internal innan något anrop görs, och
nätverkspolicyn undantar samma nät. TILLAT_INTERNA_UPPSLAG öppnar för
verkstäder som har OEM-servern på eget nät.

Delningsfiltret var en nekalista, alltså blev varje ny händelsetyp
automatiskt synlig i kundens delningslänk tills någon kom ihåg att neka
den — fel håll att fela åt på en integritetsgräns. Nu räknas i stället
upp vad som får delas per nivå, och ett test kräver att varje
händelsetyp i domänmodellen är klassificerad. Samma ändring i
Supabase-funktionen via ny migration. Beteendet i dag är oförändrat;
det är riktningen som vänts.

Ingressen saknade kroppsgräns och hade därmed nginx standard på 1 MB
medan tjänsten tar 4 MB — foto- och videodokumentation hade avvisats i
produktion men aldrig i testerna. Satt till 8 MB i båda vägarna.

Vidare: nätverkspolicyer som stänger namnrymden och bara öppnar de
faktiska flödena, CORS-lista via TILLATNA_URSPRUNG i stället för "*",
och säkerhetskontext + startprob på databaspodden.

Kustomize-/Argo CD-vägen finns kvar men ska inte köras mot samma kluster
som Terraform — selfHeal och prune motarbetar terraform apply. Val och
bytesväg dokumenterade.

Verifierat: 87 vitest-tester, typkontroll, eslint, OpenAPI-validering,
terraform fmt och integrationstest mot riktig Postgres inklusive den nya
spärren. terraform validate kunde inte köras här — registry.terraform.io
är blockerad av sessionens egress-policy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-03 14:10:59 +00:00

165 lines
3.8 KiB
Terraform

# Postgres — händelseloggen.
#
# Append-only garanteras av triggers i schemat, inte av att API:t låter
# bli att skriva. Det gör historiken oantastlig även för den som har
# databasåtkomst med rätt roll.
#
# En StatefulSet med en PVC är INTE säkerhetskopiering. Innan skarp drift:
# byt mot CloudNativePG-operatorn (basbackup + WAL-arkivering + PITR +
# failover) och peka plattformens DATABASE_URL på dess tjänst.
resource "kubernetes_stateful_set_v1" "postgres" {
metadata {
name = "postgres"
namespace = kubernetes_namespace_v1.denna.metadata[0].name
labels = merge(local.etiketter, { app = "postgres" })
}
spec {
service_name = "postgres"
replicas = 1
selector {
match_labels = { app = "postgres" }
}
template {
metadata {
labels = merge(local.etiketter, { app = "postgres" })
}
spec {
automount_service_account_token = false
security_context {
run_as_non_root = true
run_as_user = 70 # postgres i alpine-bilden
fs_group = 70
seccomp_profile {
type = "RuntimeDefault"
}
}
container {
name = "postgres"
image = local.tjanster.postgres.bild
port {
container_port = 5432
}
env {
name = "POSTGRES_DB"
value = "felsokning"
}
env {
name = "POSTGRES_USER"
value = "plattform"
}
env {
name = "POSTGRES_PASSWORD"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.hemligheter.metadata[0].name
key = "postgres-losenord"
}
}
}
env {
name = "PGDATA"
value = "/var/lib/postgresql/data/pgdata"
}
volume_mount {
name = "data"
mount_path = "/var/lib/postgresql/data"
}
volume_mount {
name = "init"
mount_path = "/docker-entrypoint-initdb.d"
}
resources {
requests = { cpu = "250m", memory = "512Mi" }
limits = { cpu = "2", memory = "2Gi" }
}
readiness_probe {
exec {
command = ["pg_isready", "-U", "plattform", "-d", "felsokning"]
}
initial_delay_seconds = 5
period_seconds = 10
}
# Startprob i stället för liveness under uppstart: en stor
# återställning får ta tid utan att poden dödas i loop.
startup_probe {
exec {
command = ["pg_isready", "-U", "plattform", "-d", "felsokning"]
}
period_seconds = 10
failure_threshold = 30
}
security_context {
allow_privilege_escalation = false
capabilities {
drop = ["ALL"]
}
}
}
volume {
name = "init"
config_map {
name = kubernetes_config_map_v1.postgres_init.metadata[0].name
}
}
}
}
volume_claim_template {
metadata {
name = "data"
}
spec {
access_modes = ["ReadWriteOnce"]
storage_class_name = var.lagringsklass != "" ? var.lagringsklass : null
resources {
requests = { storage = var.databas_storlek }
}
}
}
}
}
resource "kubernetes_service_v1" "postgres" {
metadata {
name = "postgres"
namespace = kubernetes_namespace_v1.denna.metadata[0].name
labels = merge(local.etiketter, { app = "postgres" })
}
spec {
selector = { app = "postgres" }
port {
port = 5432
target_port = 5432
}
}
}