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
This commit is contained in:
Claude
2026-08-03 14:10:59 +00:00
parent 8f1aba1571
commit cbc7bf2751
21 changed files with 1982 additions and 18 deletions
+116
View File
@@ -0,0 +1,116 @@
// @vitest-environment node
// Delningsgränsen är en integritetsgräns. Testerna här låser två saker:
// att den är en tillåtelselista (nya händelsetyper är interna tills
// någon aktivt släpper fram dem) och att uppslag mot kundkonfigurerade
// URL:er inte kan användas för att nå klustrets insida.
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import {
DELBART_KUND,
DELBART_PARTNER,
ENDAST_INTERNT,
arPrivatAdress,
gorUppslag,
pekarInat,
synligaTyper,
} from "../../../services/plattform/server.mjs";
// Alla händelsetyper i domänmodellen — sanningen om vad som kan hamna
// i loggen.
const HANDELSETYPER = [
...new Set(
[...readFileSync("src/felsokning/domain.ts", "utf8").matchAll(/\btyp:\s*"([a-z_]+)"/g)].map((m) => m[1]),
),
];
describe("delningsfiltret är en tillåtelselista", () => {
it("varje händelsetyp i domänmodellen är klassificerad", () => {
// Fångar den verkliga risken: någon lägger till en händelsetyp och
// glömmer bestämma om kunden ska se den. Då faller testet i stället
// för att typen tyst dyker upp i kundens delningslänk.
const klassificerade = new Set([...DELBART_KUND, ...ENDAST_INTERNT]);
const oklassificerade = HANDELSETYPER.filter((t) => !klassificerade.has(t));
expect(oklassificerade, `oklassificerade händelsetyper: ${oklassificerade.join(", ")}`).toEqual([]);
});
it("hittar faktiskt domänens händelsetyper", () => {
// Skyddar testet ovan mot att bli meningslöst om regexen slutar träffa.
expect(HANDELSETYPER.length).toBeGreaterThan(20);
expect(HANDELSETYPER).toContain("felorsak");
expect(HANDELSETYPER).toContain("ai_svar");
});
it("arbetsmaterial och arbetsledning lämnar aldrig organisationen", () => {
for (const intern of ENDAST_INTERNT) {
expect(DELBART_KUND, intern).not.toContain(intern);
}
// Hypoteser är det enda partnern ser utöver kundnivån.
expect(DELBART_PARTNER).toContain("hypotes");
expect(DELBART_KUND).not.toContain("hypotes");
for (const bara of ["kategori_byte", "ai_svar", "ansvarig_satt", "arbetsorder_skannad"]) {
expect(DELBART_PARTNER, bara).not.toContain(bara);
}
});
it("internnivån filtrerar inte alls", () => {
expect(synligaTyper("intern")).toBeNull();
expect(synligaTyper("kund")).toEqual(DELBART_KUND);
expect(synligaTyper("partner")).toEqual(DELBART_PARTNER);
});
});
describe("uppslag kan inte riktas mot klustrets insida", () => {
it("känner igen privata, loopback- och link-local-adresser", () => {
for (const intern of [
"127.0.0.1",
"10.4.2.9",
"172.16.0.1",
"172.31.255.255",
"192.168.1.1",
"169.254.169.254", // molnens metadatatjänst
"100.64.0.1", // CGNAT
"0.0.0.0",
"::1",
"fd00::1",
"fe80::1",
"::ffff:127.0.0.1",
]) {
expect(arPrivatAdress(intern), intern).toBe(true);
}
for (const extern of ["8.8.8.8", "51.12.3.4", "172.32.0.1", "192.169.0.1", "2606:4700::1111"]) {
expect(arPrivatAdress(extern), extern).toBe(false);
}
});
it("stoppar både IP-literaler och namn som resolvar inåt", async () => {
expect(await pekarInat("http://169.254.169.254/latest/meta-data/")).toBe("169.254.169.254");
expect(await pekarInat("http://localhost:8080/x")).toBe("localhost");
// Klustrets egna tjänstenamn slutar på .local och stoppas på namnet,
// utan att ens behöva slås upp.
expect(await pekarInat("http://postgres.guidad-felsokning.svc.cluster.local/")).toBe(
"postgres.guidad-felsokning.svc.cluster.local",
);
// Namn som slår upp till en privat adress fångas via uppslaget.
const falskUppslagare = async () => [{ address: "10.0.0.5", family: 4 }];
expect(await pekarInat("https://kund.exempel.se/", falskUppslagare)).toBe("10.0.0.5");
const publikUppslagare = async () => [{ address: "93.184.216.34", family: 4 }];
expect(await pekarInat("https://kund.exempel.se/", publikUppslagare)).toBeNull();
});
it("uppslaget avvisar en intern bas-URL innan något anrop görs", async () => {
let anropades = false;
const def = { uppslag: { urlFalt: "bas_url", auth: "bearer", authFalt: "n", svarsfalt: { marke: "make" } } };
const svar = await gorUppslag(
def,
{ bas_url: "http://169.254.169.254/{vin}", n: "x" },
"YV1DZ8256F2123456",
async () => {
anropades = true;
return { ok: true, json: async () => ({ make: "Volvo" }) };
},
);
expect(svar.ok).toBe(false);
expect(svar.fel).toContain("intern adress");
expect(anropades, "anropet får inte göras alls").toBe(false);
});
});