carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "strings"
5 "testing"
6)
7
8// directives returns every "Key=value" line of a unit, comments and
9// section headers dropped, in file order.
10func directives(u string) []string {
11 var out []string
12 for _, line := range strings.Split(u, "\n") {
13 line = strings.TrimSpace(line)
14 if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "[") {
15 continue
16 }
17 out = append(out, line)
18 }
19 return out
20}
21
22func hasDirective(u, want string) bool {
23 for _, d := range directives(u) {
24 if d == want {
25 return true
26 }
27 }
28 return false
29}
30
31// instanceDeny is the namespace block's egress fence (#214 item 3): every
32// address a tenant has no business dialling, loopback first. A tenant
33// reaches the internet; it does not reach the box, the VPC, or the cloud's
34// internal ranges. The box's resolver is re-opened per box by the control
35// service on carlos-instances.slice (IPAddressAllow=, which wins).
36const instanceDeny = "IPAddressDeny=localhost multicast " +
37 "0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 169.254.0.0/16 169.254.169.254/32 172.16.0.0/12 192.0.0.0/24 192.168.0.0/16 198.18.0.0/15 240.0.0.0/4 " +
38 "64:ff9b::/96 fc00::/7 fd00:ec2::254/128 fe80::/10 fec0::/10"
39
40// Every directive the hardening adds to the shared namespace block (#214
41// items 2 and 3, #178), pinned exactly: the block is byte-identical across
42// the four templates and the probe (TestInstanceTemplatesShareOneNamespace),
43// so pinning it on carlos-instance@ pins it on all five.
44func TestNamespaceBlockCarriesTheHardening(t *testing.T) {
45 for _, name := range []string{"carlos-instance@.service", "carlos-instance-gen@.service", "carlos-instance-seed@.service", "carlos-instance-sidecar@.service", "carlos-instance-cmd@.service", "carlos-instance-probe.service"} {
46 u := readUnit(t, name)
47 for _, want := range []string{
48 // #214 item 2: no other app's build is visible.
49 "TemporaryFileSystem=/opt/carlos:ro",
50 "BindReadOnlyPaths=/opt/carlos/carlos",
51 // #214 item 3: loopback, private and cloud-internal ranges.
52 instanceDeny,
53 // #178: SysV and POSIX mqueue namespaces of its own, and the
54 // host's mqueue mount out of sight.
55 // #214 item 3: resolved's upstreams, never its loopback stub.
56 "BindReadOnlyPaths=-/run/systemd/resolve/resolv.conf:/etc/resolv.conf",
57 "PrivateIPC=yes",
58 "TemporaryFileSystem=/dev/mqueue:ro",
59 } {
60 if !hasDirective(u, want) {
61 t.Errorf("%s lacks %q", name, want)
62 }
63 }
64 // The old IMDS-only line is gone, not left beside the new one:
65 // two IPAddressDeny= lines would still union, but a reader would
66 // take the first for the whole fence.
67 if hasDirective(u, "IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128") {
68 t.Errorf("%s still carries the IMDS-only IPAddressDeny line", name)
69 }
70 // Nothing in the namespace re-opens what the fence closes: the
71 // resolver allow lives on the slice, set per box at run time.
72 for _, d := range directives(u) {
73 if strings.HasPrefix(d, "IPAddressAllow=") {
74 t.Errorf("%s carries %q; the resolver allow belongs on carlos-instances.slice", name, d)
75 }
76 }
77 }
78}
79
80// The doc-engine fence (caps/render.go) is the platform's other private-
81// range list. The instance fence must cover every range it does, plus
82// loopback, which the doc engine deliberately leaves open.
83func TestInstanceDenyCoversTheDocEngineFence(t *testing.T) {
84 have := map[string]bool{}
85 for _, f := range strings.Fields(strings.TrimPrefix(instanceDeny, "IPAddressDeny=")) {
86 have[f] = true
87 }
88 // localhost and multicast are systemd's names for 127.0.0.0/8 ::1/128
89 // and 224.0.0.0/4 ff00::/8. Link-local is spelled out: systemd's
90 // "link-local" is fe80::/64, narrower than the fe80::/10 here.
91 named := map[string]string{"224.0.0.0/4": "multicast", "ff00::/8": "multicast"}
92 for _, r := range []string{"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "169.254.0.0/16", "169.254.169.254/32", "172.16.0.0/12",
93 "192.0.0.0/24", "192.168.0.0/16", "198.18.0.0/15", "224.0.0.0/4", "240.0.0.0/4",
94 "64:ff9b::/96", "fc00::/7", "fd00:ec2::254/128", "fe80::/10", "fec0::/10", "ff00::/8"} {
95 if !have[r] && !have[named[r]] {
96 t.Errorf("instance fence lacks %s, which the doc-engine fence denies", r)
97 }
98 }
99 if !have["localhost"] {
100 t.Error("instance fence leaves loopback open")
101 }
102}
103
104// Each template binds back exactly its own build (#214 item 2): the control
105// service writes the two sources before every start (instancectl ensureExe).
106// The cmd template binds sources of its own, cmd-app and cmd-live, which no
107// other template's start writes (merge ruling M4).
108func TestInstanceTemplatesBindOnlyTheirOwnBuild(t *testing.T) {
109 for name, src := range map[string][2]string{
110 "carlos-instance@.service": {"%I", ""},
111 "carlos-instance-seed@.service": {"%I", ""},
112 "carlos-instance-sidecar@.service": {"%I", ""},
113 "carlos-instance-cmd@.service": {"%I", "cmd-"},
114 "carlos-instance-probe.service": {"_probe", ""},
115 } {
116 u := readUnit(t, name)
117 iid, pre := src[0], src[1]
118 want := "BindReadOnlyPaths=/run/carlos-control/exe/" + iid + "/" + pre + "app:/opt/carlos/app /run/carlos-control/exe/" + iid + "/" + pre + "live:/opt/carlos/live"
119 if !hasDirective(u, want) {
120 t.Errorf("%s lacks %q", name, want)
121 }
122 for _, d := range directives(u) {
123 if strings.Contains(d, "/opt/carlos/versions") {
124 t.Errorf("%s names the version store: %q", name, d)
125 }
126 }
127 }
128}
129
130// The probe's own resource section carries a CPU and IO setting it can read
131// back from its own cgroup (#214 item 1): proof that systemd applies them
132// under carlos-instances.slice on this box, which is where the per-host
133// slices get the tier's.
134func TestProbeCarriesReadableCPUAndIOSettings(t *testing.T) {
135 u := readUnit(t, "carlos-instance-probe.service")
136 for _, want := range []string{"CPUQuota=50%", "CPUWeight=50", "IOWeight=50"} {
137 if !hasDirective(u, want) {
138 t.Errorf("probe lacks %q", want)
139 }
140 }
141}
142
143// carlos-app@ gets the same /opt view (#214 item 2): its ExecStart is
144// /opt/carlos/live/%i, which PID 1 chases to the build when it binds it.
145func TestAppTemplateSeesOnlyItsOwnBuild(t *testing.T) {
146 u := readUnit(t, "carlos-app@.service")
147 for _, want := range []string{"TemporaryFileSystem=/opt/carlos:ro", "BindReadOnlyPaths=/opt/carlos/live/%i", "ExecStart=/opt/carlos/live/%i serve"} {
148 if !hasDirective(u, want) {
149 t.Errorf("carlos-app@.service lacks %q", want)
150 }
151 }
152}
153