package caps import ( "bytes" "encoding/json" "os" "strings" "testing" ) // keymailSet is the full declared set the worked example in spec §4a // renders from: command, runtime_dir, children, host_role and a single // privileged port, plus a `data` capability that (being self-service and // directive-free) must never show up in the rendered bytes. func keymailSet(t *testing.T) *Set { t.Helper() var s Set raw := `{ "v": 1, "command": "relay", "daemon": true, "children": true, "runtime_dir": "keymail", "ports": [{"port": 25, "name": "mx"}], "data": {"replicate": "platform", "dbs": ["registry.db"]}, "host_role": true }` if err := json.Unmarshal([]byte(raw), &s); err != nil { t.Fatalf("decode keymail set: %v", err) } return &s } func keymailGrants() Grants { return Grants{Ports: map[int]bool{25: true}, HostRole: true} } func consoleSet(t *testing.T) *Set { t.Helper() var s Set raw := `{"v": 1, "daemon": true, "host_role": true}` if err := json.Unmarshal([]byte(raw), &s); err != nil { t.Fatalf("decode console set: %v", err) } return &s } func consoleGrants() Grants { return Grants{HostRole: true} } // gotenbergSet is the worked example for the doc_engine capability: a // document-conversion orchestrator that spawns children (Chromium, // LibreOffice) and is granted the engine trade. func gotenbergSet(t *testing.T) *Set { t.Helper() var s Set raw := `{ "v": 1, "command": "serve", "daemon": true, "children": true, "doc_engine": true }` if err := json.Unmarshal([]byte(raw), &s); err != nil { t.Fatalf("decode gotenberg set: %v", err) } return &s } func gotenbergGrants() Grants { return Grants{DocEngine: true} } func readGolden(t *testing.T, name string) []byte { t.Helper() b, err := os.ReadFile("testdata/" + name) if err != nil { t.Fatalf("read golden %s: %v", name, err) } return b } func TestRenderServiceMatchesKeymailGolden(t *testing.T) { got, err := RenderService("keymail", keymailSet(t), keymailGrants()) if err != nil { t.Fatalf("RenderService: %v", err) } want := readGolden(t, "keymail.service.conf") if !bytes.Equal(got, want) { t.Fatalf("RenderService mismatch:\n got: %q\n want: %q", got, want) } } func TestRenderSocketMatchesKeymailGolden(t *testing.T) { got, err := RenderSocket(keymailSet(t), keymailGrants()) if err != nil { t.Fatalf("RenderSocket: %v", err) } want := readGolden(t, "keymail.socket.conf") if !bytes.Equal(got, want) { t.Fatalf("RenderSocket mismatch:\n got: %q\n want: %q", got, want) } } func TestRenderServiceMatchesConsoleGolden(t *testing.T) { got, err := RenderService("console", consoleSet(t), consoleGrants()) if err != nil { t.Fatalf("RenderService: %v", err) } want := readGolden(t, "console.service.conf") if !bytes.Equal(got, want) { t.Fatalf("RenderService mismatch:\n got: %q\n want: %q", got, want) } } func TestRenderServiceMatchesGotenbergGolden(t *testing.T) { got, err := RenderService("gotenberg", gotenbergSet(t), gotenbergGrants()) if err != nil { t.Fatalf("RenderService: %v", err) } want := readGolden(t, "gotenberg.service.conf") if !bytes.Equal(got, want) { t.Fatalf("RenderService mismatch:\n got: %q\n want: %q", got, want) } } // TestDocEngineTradeIsAtomic is the load-bearing property of this capability: // the resource relaxation and the egress fence land together or not at all. A // granted engine that got JIT and memory WITHOUT the private-network fence // would be an open SSRF proxy into the VPC with a bigger blast radius than the // stock tenant — the exact opposite of the trade the operator consented to. So // every relaxation directive must coexist with the fence in the same bytes. func TestDocEngineTradeIsAtomic(t *testing.T) { got, err := RenderService("gotenberg", gotenbergSet(t), gotenbergGrants()) if err != nil { t.Fatalf("RenderService: %v", err) } relaxations := []string{"MemoryDenyWriteExecute=no", "MemoryMax=" + docEngineMemoryMax, "TasksMax=" + docEngineTasksMax} fence := "IPAddressDeny=0.0.0.0/8" hasFence := bytes.Contains(got, []byte(fence)) if !hasFence { t.Fatalf("engine drop-in has no egress fence:\n%s", got) } for _, r := range relaxations { if !bytes.Contains(got, []byte(r)) { t.Errorf("engine drop-in is missing relaxation %q (the fence is present, so the trade is half-written):\n%s", r, got) } } } // TestDocEngineFenceCoversThePrivateRangesAndNeverLoopback pins the two halves // of the fence that are each independently load-bearing and each easy to get // wrong: // // - every private/reserved range an SSRF'd engine would be pointed at is // denied, IMDS named explicitly (not only covered by its supernet); // - loopback is NEVER denied, because Gotenberg drives its own Chromium and // LibreOffice over 127.0.0.1 and reaches its DNS stub resolver there too. // Fencing loopback bricks the service and, on AWS, breaks name resolution // outright (the VPC resolver is at VPC-base+2, inside the denied RFC1918 // ranges, so the box must hand the tenant a loopback stub). func TestDocEngineFenceCoversThePrivateRangesAndNeverLoopback(t *testing.T) { got, err := RenderService("gotenberg", gotenbergSet(t), gotenbergGrants()) if err != nil { t.Fatalf("RenderService: %v", err) } deny := "" for _, line := range strings.Split(string(got), "\n") { if strings.HasPrefix(line, "IPAddressDeny=") && line != "IPAddressDeny=" { deny = line } } for _, mustDeny := range []string{ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // RFC1918 "169.254.169.254/32", "fd00:ec2::254/128", // IMDS, named explicitly "169.254.0.0/16", "fe80::/10", // link-local "100.64.0.0/10", // CGNAT (cloud internal services) "fc00::/7", // IPv6 ULA "192.0.0.0/24", "240.0.0.0/4", // IETF assignments, reserved "198.18.0.0/15", // benchmarking / common NAT-proxy range "64:ff9b::/96", // NAT64 well-known prefix (translates into private IPv4) "fec0::/10", // IPv6 site-local (deprecated but routable on some VPCs) } { if !strings.Contains(deny, mustDeny) { t.Errorf("egress fence does not deny %q:\n%s", mustDeny, deny) } } for _, mustNotDeny := range []string{"127.0.0.0/8", "127.0.0.1", "::1/128", "localhost"} { if strings.Contains(deny, mustNotDeny) { t.Errorf("egress fence denies loopback %q — Gotenberg talks to its own engines and its DNS stub resolver over loopback; fencing it bricks the service:\n%s", mustNotDeny, deny) } } } // TestDocEngineBandReplacesTheChildrenBand pins the scalar-render-once // property: a conversion orchestrator declares children AND doc_engine, and the // engine band replaces the children band's memory/tasks rather than stacking a // second assignment on top. The generated unit text must never contain two // MemoryMax or two TasksMax lines, so it does not lean on systemd's // last-assignment-wins rule — a reader sees the effective value directly. // KillMode, which is children's "stop kills the tree" promise and not a // resource scalar, still renders. func TestDocEngineBandReplacesTheChildrenBand(t *testing.T) { got, err := RenderService("gotenberg", gotenbergSet(t), gotenbergGrants()) if err != nil { t.Fatalf("RenderService: %v", err) } for _, scalar := range []string{"MemoryMax=", "TasksMax=", "MemoryHigh="} { n := 0 for _, line := range strings.Split(string(got), "\n") { if strings.HasPrefix(line, scalar) { n++ } } if n != 1 { t.Errorf("engine drop-in renders %q %d times, want exactly once (the children band must be replaced, not stacked):\n%s", scalar, n, got) } } if !bytes.Contains(got, []byte("KillMode=control-group")) { t.Errorf("engine drop-in lost children's KillMode:\n%s", got) } if bytes.Contains(got, []byte("MemoryMax=1G")) { t.Errorf("engine drop-in still carries the children tier's 1G — the engine band did not replace it:\n%s", got) } } // TestDocEngineUngrantedRendersNoRelaxationAndNoFence is the asymmetric case: // a manifest declaring doc_engine on a box that has NOT granted it materializes // neither the relaxation nor the fence. The engine simply will not start under // the stock template (MemoryDenyWriteExecute stays on, the default tier // OOM-kills it) — which is the safe direction, and ungrantedAdvice names the // fix. It must never render the fence alone, either: a fence with no engine is // a lie in the unit text. func TestDocEngineUngrantedRendersNoRelaxationAndNoFence(t *testing.T) { got, err := RenderService("gotenberg", gotenbergSet(t), Grants{}) if err != nil { t.Fatalf("RenderService: %v", err) } for _, unwanted := range []string{"MemoryDenyWriteExecute=no", "IPAddressDeny=0.0.0.0/8", "MemoryMax=" + docEngineMemoryMax} { if bytes.Contains(got, []byte(unwanted)) { t.Errorf("ungranted doc_engine rendered %q:\n%s", unwanted, got) } } // children is still declared and self-service, so its band renders. if !bytes.Contains(got, []byte("MemoryMax=1G")) { t.Errorf("ungranted doc_engine lost the children band it still declares:\n%s", got) } } // TestDocEngineGrantAloneRendersNothing mirrors the host_role grant-lingers // case: a grant with no matching declaration materializes nothing. The grant is // not a declaration. func TestDocEngineGrantAloneRendersNothing(t *testing.T) { s := &Set{V: 1, Command: "serve"} got, err := RenderService("gotenberg", s, Grants{DocEngine: true}) if err != nil { t.Fatalf("RenderService: %v", err) } if bytes.Contains(got, []byte("IPAddressDeny=0.0.0.0/8")) || bytes.Contains(got, []byte("MemoryDenyWriteExecute=no")) { t.Errorf("a doc_engine grant with no declaration rendered engine directives:\n%s", got) } } // TestBothGrantsHeldOneDeclaredRendersOnlyTheDeclaredBlock pins the case the // host_role+doc_engine refusal at declaration time cannot cover by itself: the // registry's caps column CAN hold both tokens (ValidGrantToken accepts each, // and nothing forbids the pair in the column — the refusal lives in // ValidCapabilities, which sees the manifest). So a box whose column says // "doc-engine,host-role" against a manifest declaring only one must render that // one block and NOTHING of the other — in particular, a doc_engine declaration // must never pick up host_role's IPAddressAllow, which in systemd's BPF // firewall would win over the fence's IPAddressDeny and re-open IMDS. func TestBothGrantsHeldOneDeclaredRendersOnlyTheDeclaredBlock(t *testing.T) { both := Grants{DocEngine: true, HostRole: true} engineOnly, err := RenderService("gotenberg", &Set{V: 1, Command: "serve", DocEngine: true}, both) if err != nil { t.Fatalf("RenderService(doc_engine only): %v", err) } if !bytes.Contains(engineOnly, []byte("IPAddressDeny=0.0.0.0/8")) { t.Errorf("doc_engine declaration under both grants lost its fence:\n%s", engineOnly) } for _, forbidden := range []string{"IPAddressAllow", "DynamicUser=no", "User=carlos-gotenberg"} { if bytes.Contains(engineOnly, []byte(forbidden)) { t.Errorf("doc_engine declaration under both grants rendered host_role's %q:\n%s", forbidden, engineOnly) } } hostOnly, err := RenderService("keymail", &Set{V: 1, Command: "relay", HostRole: true}, both) if err != nil { t.Fatalf("RenderService(host_role only): %v", err) } if !bytes.Contains(hostOnly, []byte("IPAddressAllow=169.254.169.254/32")) { t.Errorf("host_role declaration under both grants lost its IPAddressAllow:\n%s", hostOnly) } if bytes.Contains(hostOnly, []byte("IPAddressDeny=0.0.0.0/8")) || bytes.Contains(hostOnly, []byte("MemoryDenyWriteExecute=no")) { t.Errorf("host_role declaration under both grants rendered engine directives:\n%s", hostOnly) } } // TestRenderIsDeterministic renders the same effective set from two // independently-decoded Sets whose "ports" arrays are given in different // orders, and asserts byte-identical output with ports in ascending order // regardless of declaration order. func TestRenderIsDeterministic(t *testing.T) { const a = `{"v":1,"command":"relay","ports":[{"port":587,"name":"submission"},{"port":25,"name":"mx"}]}` const b = `{"v":1,"command":"relay","ports":[{"port":25,"name":"mx"},{"port":587,"name":"submission"}]}` var sa, sb Set if err := json.Unmarshal([]byte(a), &sa); err != nil { t.Fatalf("decode a: %v", err) } if err := json.Unmarshal([]byte(b), &sb); err != nil { t.Fatalf("decode b: %v", err) } g := Grants{Ports: map[int]bool{25: true, 587: true}} gotA, err := RenderSocket(&sa, g) if err != nil { t.Fatalf("RenderSocket(a): %v", err) } gotB, err := RenderSocket(&sb, g) if err != nil { t.Fatalf("RenderSocket(b): %v", err) } if !bytes.Equal(gotA, gotB) { t.Fatalf("render not deterministic across declaration order:\n a: %q\n b: %q", gotA, gotB) } want := []byte(Marker + "\n[Socket]\nListenStream=25\nListenStream=587\n") if !bytes.Equal(gotA, want) { t.Fatalf("ports not sorted ascending: %q", gotA) } // Rendering again from the same Set must reproduce the identical bytes. again, err := RenderSocket(&sa, g) if err != nil { t.Fatalf("RenderSocket(a) again: %v", err) } if !bytes.Equal(gotA, again) { t.Fatalf("render not repeatable: %q vs %q", gotA, again) } svcA, err := RenderService("keymail", &sa, g) if err != nil { t.Fatalf("RenderService(a): %v", err) } svcAgain, err := RenderService("keymail", &sa, g) if err != nil { t.Fatalf("RenderService(a) again: %v", err) } if !bytes.Equal(svcA, svcAgain) { t.Fatalf("service render not repeatable: %q vs %q", svcA, svcAgain) } } // TestUngrantedCapabilitiesMaterializeNothing: keymail's full set with no // grants at all still renders the self-service lines (command, runtime_dir, // children) but none of the operator-consented ones — no DynamicUser/ // RestrictSUIDSGID/User/Group, and a nil socket body. keymail with no // port:25 grant serves HTTP and does not bind the MX; it does not refuse // to start. func TestUngrantedCapabilitiesMaterializeNothing(t *testing.T) { got, err := RenderService("keymail", keymailSet(t), Grants{}) if err != nil { t.Fatalf("RenderService: %v", err) } if got == nil { t.Fatal("service body is nil, want the self-service lines") } for _, want := range []string{ "ExecStart=\n", "ExecStart=/opt/carlos/live/%i relay\n", "RuntimeDirectory=keymail\n", "KillMode=control-group\n", } { if !bytes.Contains(got, []byte(want)) { t.Errorf("service body missing self-service line %q:\n%s", want, got) } } for _, unwanted := range []string{"DynamicUser", "RestrictSUIDSGID", "User=", "Group=", "IPAddressAllow"} { if bytes.Contains(got, []byte(unwanted)) { t.Errorf("service body has ungranted host_role directive %q:\n%s", unwanted, got) } } sock, err := RenderSocket(keymailSet(t), Grants{}) if err != nil { t.Fatalf("RenderSocket: %v", err) } if sock != nil { t.Fatalf("socket body = %q, want nil when port:25 is not granted", sock) } } // TestHostRoleGrantLingersButManifestNoLongerDeclaresFallsBackToDynamicUser // covers the asymmetric case where an operator grant survives a build that // stopped asking for it: the grant alone must never materialize // DynamicUser=no/RestrictSUIDSGID=yes/User=/Group= — only a Set that STILL // declares host_role does. func TestHostRoleGrantLingersButManifestNoLongerDeclaresFallsBackToDynamicUser(t *testing.T) { s := &Set{V: 1, Command: "relay"} got, err := RenderService("keymail", s, Grants{HostRole: true}) if err != nil { t.Fatalf("RenderService: %v", err) } for _, want := range []string{"ExecStart=\n", "ExecStart=/opt/carlos/live/%i relay\n"} { if !bytes.Contains(got, []byte(want)) { t.Errorf("service body missing %q:\n%s", want, got) } } for _, unwanted := range []string{"DynamicUser", "RestrictSUIDSGID", "User=", "Group=", "IPAddressAllow"} { if bytes.Contains(got, []byte(unwanted)) { t.Errorf("service body has host_role directive %q though the manifest no longer declares host_role:\n%s", unwanted, got) } } } // TestRenderSocketOnlyEmitsGrantedPorts: a Set declaring two ports with // only one of them granted must emit exactly that one ListenStream= line. func TestRenderSocketOnlyEmitsGrantedPorts(t *testing.T) { s := &Set{V: 1, Ports: []Port{{Port: 25, Name: "mx"}, {Port: 587, Name: "submission"}}} got, err := RenderSocket(s, Grants{Ports: map[int]bool{587: true}}) if err != nil { t.Fatalf("RenderSocket: %v", err) } want := []byte(Marker + "\n[Socket]\nListenStream=587\n") if !bytes.Equal(got, want) { t.Fatalf("got %q, want %q", got, want) } } // TestRenderSocketGrantOfAnUndeclaredPortRendersNil: a grant naming a port // the Set never declared materializes nothing — the grant alone is not a // declaration. func TestRenderSocketGrantOfAnUndeclaredPortRendersNil(t *testing.T) { s := &Set{V: 1, Ports: []Port{{Port: 25, Name: "mx"}}} got, err := RenderSocket(s, Grants{Ports: map[int]bool{587: true}}) if err != nil { t.Fatalf("RenderSocket: %v", err) } if got != nil { t.Fatalf("got %q, want nil — the grant names a port the manifest never declared", got) } } // TestEmptyEffectiveSetRendersNil: daemon and data emit no directives, and // nothing else is declared, so both bodies are nil. func TestEmptyEffectiveSetRendersNil(t *testing.T) { s := &Set{V: 1, Daemon: true, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{"console.db"}}} got, err := RenderService("console", s, Grants{}) if err != nil { t.Fatalf("RenderService: %v", err) } if got != nil { t.Fatalf("RenderService = %q, want nil", got) } sock, err := RenderSocket(s, Grants{}) if err != nil { t.Fatalf("RenderSocket: %v", err) } if sock != nil { t.Fatalf("RenderSocket = %q, want nil", sock) } } // TestNilSetRendersNil: a nil *Set (every manifest before this feature, and // every route that never declared capabilities) must render (nil, nil), // not panic and not error. func TestNilSetRendersNil(t *testing.T) { got, err := RenderService("console", nil, Grants{HostRole: true}) if err != nil { t.Fatalf("RenderService(nil): %v", err) } if got != nil { t.Fatalf("RenderService(nil) = %q, want nil", got) } sock, err := RenderSocket(nil, Grants{Ports: map[int]bool{25: true}}) if err != nil { t.Fatalf("RenderSocket(nil): %v", err) } if sock != nil { t.Fatalf("RenderSocket(nil) = %q, want nil", sock) } } // TestRenderServiceRefusesAnInvalidSet closes the direct-injection path: a // *Set built by hand, never having passed through UnmarshalJSON's strict // decode, still has to clear ValidCapabilities before anything renders. A // command with an embedded newline would otherwise inject a second // directive line into the unit. func TestRenderServiceRefusesAnInvalidSet(t *testing.T) { bad := &Set{V: 1, Command: "relay\nUser=root"} if _, err := RenderService("keymail", bad, Grants{}); err == nil { t.Fatal("RenderService accepted a command with an embedded newline") } } // TestRenderServiceRefusesHostRoleWithAnEmptyApp: an empty app would // otherwise render "User=carlos-" — a real, if odd, system user name that // names no app. ValidCapabilities's app-charset check refuses it here. func TestRenderServiceRefusesHostRoleWithAnEmptyApp(t *testing.T) { s := &Set{V: 1, HostRole: true} if _, err := RenderService("", s, Grants{HostRole: true}); err == nil { t.Fatal("RenderService accepted host_role with an empty app name") } } // TestRenderSocketRefusesAnInvalidSet: the same direct-injection path, // for RenderSocket — a non-privileged or reserved port built by hand must // still be refused before it renders a ListenStream= line. func TestRenderSocketRefusesAnInvalidSet(t *testing.T) { bad := &Set{V: 1, Ports: []Port{{Port: 8080}}} if _, err := RenderSocket(bad, Grants{Ports: map[int]bool{8080: true}}); err == nil { t.Fatal("RenderSocket accepted a non-privileged port") } } // TestRenderStartsWithTheMarker checks every non-nil body this file // produces opens with the marker line. func TestRenderStartsWithTheMarker(t *testing.T) { svc, err := RenderService("keymail", keymailSet(t), keymailGrants()) if err != nil { t.Fatalf("RenderService(keymail): %v", err) } sock, err := RenderSocket(keymailSet(t), keymailGrants()) if err != nil { t.Fatalf("RenderSocket(keymail): %v", err) } consoleSvc, err := RenderService("console", consoleSet(t), consoleGrants()) if err != nil { t.Fatalf("RenderService(console): %v", err) } bodies := [][]byte{svc, sock, consoleSvc} for i, b := range bodies { if b == nil { t.Fatalf("body %d is nil, want non-nil to check its marker", i) } if !bytes.HasPrefix(b, []byte(Marker+"\n")) { t.Errorf("body %d does not start with the marker:\n%s", i, b) } } } // A command identical to the template's default ("serve") is still an // explicit declaration: the reset+rewrite pair is still correct and still // deterministic, even though the rewritten value matches what the template // already says. func TestCommandIdenticalToTemplateDefaultStillEmitsReset(t *testing.T) { s := &Set{V: 1, Command: "serve"} got, err := RenderService("console", s, Grants{}) if err != nil { t.Fatalf("RenderService: %v", err) } want := []byte(Marker + "\n[Service]\nExecStart=\nExecStart=/opt/carlos/live/%i serve\n") if !bytes.Equal(got, want) { t.Fatalf("got %q, want %q", got, want) } }