d3cae27fa4
Foton, video och instrumentbilder låg som data-URL:er inne i händelserna. Det drabbade allt som läser loggen: synken drog med hela bildmassan var femtonde sekund, kundvyn likaså, och en säkerhetskopia av loggen var i praktiken en kopia av alla foton. Nu ligger innehållet utanför händelsen och loggen bär en referens med innehållets SHA-256. Det försvagar inte bevisvärdet utan stärker det: hashen står i den append-only-skyddade loggen, så en bild som bytts ut går att upptäcka. Tidigare låg bilden i loggen och måste helt enkelt tros på. Innehållet kontrolleras mot hashen varje gång det lämnas ut — stämmer det inte svarar tjänsten 409 i stället för att visa bilden. Innehållsadresserat, så samma foto som dokumenteras två gånger lagras en gång. Två lägen: databas (bytea, fungerar överallt utan konfiguration) och s3 (AWS, MinIO, Ceph). Signeringen är egen — SigV4 för PUT och GET — i stället för molnleverantörens SDK, eftersom två operationer inte motiverar tiotals megabyte beroenden i en bild som annars bara har pg-drivrutinen. Den korsverifieras bit för bit mot botocore i testerna. Det avslöjade en riktig bugg direkt: host-huvudet saknade portnummer, vilket hade fungerat mot AWS men avvisats av all självhostad S3. Kodningen av objektnycklar kanoniseras medvetet inte. S3 följer andra URL-regler än övriga AWS-tjänster och en felgissad regel ger signaturer som ser rimliga ut men avvisas. I stället begränsas hink och prefix till tecken som aldrig behöver kodas — då finns ingen regel att gissa fel på. Delningsgränsen gäller även bilagor: en bilaga lämnas bara ut via en delningslänk om händelsen den hör till är synlig på den nivån, så den skannade arbetsordern nås aldrig via kundlänken. Uppladdningen sker på ett enda ställe — sidans egna skicka() flyttar innehållet innan händelsen skrivs, så ingen panel behövde ändras. Misslyckas det, eller saknas server som i lokalt läge, bäddas det in precis som förut. Dokumentationen får aldrig gå förlorad för att nätet ligger nere, och äldre händelser med inbäddad data-URL fortsätter fungera för alltid eftersom loggen är append-only. Verifierat: 96 vitest-tester (varav 9 nya för signering och innehållsadressering), typkontroll, eslint, OpenAPI-validering, terraform fmt och referenskontroll, samt integrationstest mot riktig Postgres med 13 nya kontroller — bland annat att ett manipulerat innehåll upptäcks och inte lämnas ut, och att kundlänken når fotot men inte arbetsordern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
312 lines
8.8 KiB
Terraform
312 lines
8.8 KiB
Terraform
# Allt som skiljer en installation från en annan. Inget annat ska
|
|
# behöva ändras i .tf-filerna för att driftsätta i en ny miljö.
|
|
|
|
# ---- Kluster -----------------------------------------------------------
|
|
|
|
variable "kubeconfig" {
|
|
description = "Sökväg till kubeconfig."
|
|
type = string
|
|
default = "~/.kube/config"
|
|
}
|
|
|
|
variable "kube_context" {
|
|
description = "Kubernetes-kontext att driftsätta i. Tom = kubeconfigens aktuella."
|
|
type = string
|
|
default = ""
|
|
}
|
|
|
|
variable "namnrymd" {
|
|
description = "Namnrymd för hela systemet."
|
|
type = string
|
|
default = "guidad-felsokning"
|
|
}
|
|
|
|
variable "miljo" {
|
|
description = "Miljöns namn (produktion, test, demo). Sätts som label på allt."
|
|
type = string
|
|
default = "produktion"
|
|
}
|
|
|
|
# ---- Domän och certifikat ---------------------------------------------
|
|
|
|
variable "doman" {
|
|
description = "Domänen klienten och API:t nås på, t.ex. app.exempel.se."
|
|
type = string
|
|
}
|
|
|
|
variable "cert_issuer" {
|
|
description = "cert-manager ClusterIssuer som utfärdar TLS-certifikatet."
|
|
type = string
|
|
default = "letsencrypt"
|
|
}
|
|
|
|
variable "ingress_klass" {
|
|
description = "IngressClass. Nätverkspolicyn öppnar för ingress-kontrollerns namnrymd nedan."
|
|
type = string
|
|
default = "nginx"
|
|
}
|
|
|
|
variable "ingress_namnrymd" {
|
|
description = "Namnrymd där ingress-kontrollern kör — behövs av nätverkspolicyn."
|
|
type = string
|
|
default = "ingress-nginx"
|
|
}
|
|
|
|
# ---- Bilder ------------------------------------------------------------
|
|
|
|
variable "register" {
|
|
description = "Containerregister, t.ex. ghcr.io/min-organisation."
|
|
type = string
|
|
}
|
|
|
|
variable "bildtagg" {
|
|
description = <<-TEXT
|
|
Taggen alla tre bilderna körs med — normalt git-SHA:t från
|
|
publiceringsflödet. "latest" duger för prov men gör en rullning
|
|
omöjlig att spåra.
|
|
TEXT
|
|
type = string
|
|
|
|
validation {
|
|
condition = var.bildtagg != ""
|
|
error_message = "Ange en bildtagg — helst git-SHA:t, aldrig tomt."
|
|
}
|
|
}
|
|
|
|
# ---- Hemligheter -------------------------------------------------------
|
|
#
|
|
# Sätts helst via en secrets-hanterare (External Secrets, Vault, molnets
|
|
# secret manager) i stället för tfvars. Lämnas de tomma genererar
|
|
# Terraform slumpvärden vid första körningen och behåller dem i
|
|
# tillståndet — bekvämt för prov, men då måste tillståndet skyddas
|
|
# därefter.
|
|
|
|
variable "anthropic_api_nyckel" {
|
|
description = "Claude API-nyckel. Ägs av plattformen, når aldrig klienten."
|
|
type = string
|
|
sensitive = true
|
|
}
|
|
|
|
variable "jwt_hemlighet" {
|
|
description = "HS256-hemlighet. Delas av plattformen och orkestern. Tom = genereras."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "postgres_losenord" {
|
|
description = "Databaslösenord. Tom = genereras."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "integration_nyckel" {
|
|
description = <<-TEXT
|
|
32 byte hex som krypterar kundernas leverantörsuppgifter i vila.
|
|
Tom = genereras. Byts nyckeln måste alla kopplingar sparas om —
|
|
tjänsten visar då inga värden i stället för att gissa.
|
|
TEXT
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
# ---- Drift -------------------------------------------------------------
|
|
|
|
variable "registrering_oppen" {
|
|
description = "Om vem som helst får skapa en ny organisation. Stäng i produktion."
|
|
type = bool
|
|
default = false
|
|
}
|
|
|
|
variable "tillat_interna_uppslag" {
|
|
description = <<-TEXT
|
|
Tillåter märkesspecifika uppslag mot privata nät. Behövs bara när
|
|
verkstaden har en OEM-server på sitt eget nät. Öppnar samtidigt en
|
|
väg från en tenant-administratör in i klustrets interna adresser —
|
|
lämna av om ni inte vet att ni behöver det.
|
|
TEXT
|
|
type = bool
|
|
default = false
|
|
}
|
|
|
|
variable "max_kropp_mb" {
|
|
description = <<-TEXT
|
|
Största tillåtna anropskropp. Foton och videoklipp följer med i
|
|
händelseloggen, så ingressens gräns måste minst motsvara tjänstens
|
|
egen (4 MB) — annars avvisas dokumentationen redan vid dörren.
|
|
TEXT
|
|
type = number
|
|
default = 8
|
|
}
|
|
|
|
variable "repliker" {
|
|
description = "Grundantal repliker per tjänst (autoskalning tar över uppåt)."
|
|
type = object({
|
|
web = number
|
|
plattform = number
|
|
orkester = number
|
|
})
|
|
default = {
|
|
web = 2
|
|
plattform = 2
|
|
orkester = 2
|
|
}
|
|
}
|
|
|
|
variable "max_repliker" {
|
|
description = "Tak för autoskalningen per tjänst."
|
|
type = object({
|
|
web = number
|
|
plattform = number
|
|
orkester = number
|
|
})
|
|
default = {
|
|
web = 10
|
|
plattform = 10
|
|
orkester = 10
|
|
}
|
|
}
|
|
|
|
# ---- Databas -----------------------------------------------------------
|
|
|
|
variable "databas_lage" {
|
|
description = <<-TEXT
|
|
Hur händelseloggen lagras. Inget standardvärde: valet avgör om det
|
|
finns säkerhetskopiering, och det är inte ett val någon ska göra av
|
|
misstag.
|
|
|
|
"extern" Managerad Postgres utanför klustret (Cloud SQL, RDS,
|
|
Neon, Azure Flexible Server). Leverantören sköter
|
|
backup, PITR, failover och kryptering. REKOMMENDERAT
|
|
i produktion. Kräver databas_url.
|
|
|
|
"cnpg" CloudNativePG i klustret: basbackup, WAL-arkivering och
|
|
PITR mot objektlagring. Kräver att operatorn redan är
|
|
installerad — Terraform slår upp dess CRD vid plan.
|
|
Kräver backup-uppgifterna nedan.
|
|
|
|
"inbyggd" En StatefulSet med en volym. INGEN säkerhetskopiering.
|
|
Går volymen förlorad är händelseloggen borta, och den
|
|
är hela produktens bevisvärde. Endast för prov och demo.
|
|
TEXT
|
|
type = string
|
|
|
|
validation {
|
|
condition = contains(["extern", "cnpg", "inbyggd"], var.databas_lage)
|
|
error_message = "databas_lage måste vara \"extern\", \"cnpg\" eller \"inbyggd\"."
|
|
}
|
|
}
|
|
|
|
variable "databas_url" {
|
|
description = "Anslutning till den externa databasen. Krävs när databas_lage = \"extern\"."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "backup" {
|
|
description = <<-TEXT
|
|
Objektlagring för CloudNativePG:s basbackup och WAL-arkiv. Används
|
|
bara när databas_lage = "cnpg". mal är en S3-URL, t.ex.
|
|
s3://verkstad-backup/felsokning.
|
|
TEXT
|
|
type = object({
|
|
mal = string
|
|
endpoint = optional(string, "")
|
|
behall_dagar = optional(number, 30)
|
|
})
|
|
default = {
|
|
mal = ""
|
|
}
|
|
}
|
|
|
|
variable "backup_nyckel_id" {
|
|
description = "Åtkomstnyckel till objektlagringen (databas_lage = \"cnpg\")."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "backup_nyckel" {
|
|
description = "Hemlig nyckel till objektlagringen (databas_lage = \"cnpg\")."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "databas_instanser" {
|
|
description = "Antal Postgres-instanser i CloudNativePG-klustret. 3 ger failover utan dataförlust."
|
|
type = number
|
|
default = 3
|
|
}
|
|
|
|
# ---- Bilagor -----------------------------------------------------------
|
|
|
|
variable "bilage_lage" {
|
|
description = <<-TEXT
|
|
Var foton och videoklipp lagras.
|
|
|
|
"databas" bytea i en egen tabell. Fungerar överallt och kräver
|
|
ingen konfiguration, men bilderna följer med databasens
|
|
säkerhetskopior och gör dem stora.
|
|
|
|
"s3" S3-kompatibel objektlagring (AWS, MinIO, Ceph). Loggen
|
|
och bilderna växer oberoende av varandra. Kräver
|
|
s3-uppgifterna nedan.
|
|
|
|
Innehållet är innehållsadresserat i båda lägena: samma foto lagras
|
|
en gång, och hashen står i händelseloggen så en utbytt bild går att
|
|
upptäcka.
|
|
TEXT
|
|
type = string
|
|
default = "databas"
|
|
|
|
validation {
|
|
condition = contains(["databas", "s3"], var.bilage_lage)
|
|
error_message = "bilage_lage måste vara \"databas\" eller \"s3\"."
|
|
}
|
|
}
|
|
|
|
variable "s3" {
|
|
description = "Objektlagring för bilagor. Används bara när bilage_lage = \"s3\"."
|
|
type = object({
|
|
endpoint = string
|
|
hink = string
|
|
region = string
|
|
prefix = optional(string, "bilagor")
|
|
})
|
|
default = {
|
|
endpoint = ""
|
|
hink = ""
|
|
region = ""
|
|
}
|
|
}
|
|
|
|
variable "s3_nyckel_id" {
|
|
description = "Åtkomstnyckel till bilagornas objektlagring."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "s3_nyckel" {
|
|
description = "Hemlig nyckel till bilagornas objektlagring."
|
|
type = string
|
|
sensitive = true
|
|
default = ""
|
|
}
|
|
|
|
variable "databas_storlek" {
|
|
description = "Volymstorlek för händelseloggen. Foton och video ligger inline i loggen."
|
|
type = string
|
|
default = "50Gi"
|
|
}
|
|
|
|
variable "lagringsklass" {
|
|
description = "StorageClass för databasvolymen. Tom = klustrets standard."
|
|
type = string
|
|
default = ""
|
|
}
|