carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "bytes"
7 "context"
8 "errors"
9 "log"
10 "net"
11 "net/http"
12 "os"
13 "path/filepath"
14 "strings"
15 "sync"
16 "testing"
17 "time"
18
19 "golang.org/x/sys/unix"
20
21 "github.com/carlosframework/platform/internal/moves"
22 "github.com/carlosframework/platform/internal/release"
23)
24
25// TestHeldReleaseRootRefusesUnsafeRenderJournals (#267, platform-1e): the
26// render-hash journal lives under the agent's /data, so root reads it only
27// through the anchored path: no symlink at any component, a regular file
28// with one link, one descriptor, a bounded size. A journal that is a
29// symlink to a root-only file holding the right hash, a directory on its
30// path that is a symlink to such a journal, and a FIFO are each refused at
31// once, and root selects and publishes nothing.
32func TestHeldReleaseRootRefusesUnsafeRenderJournals(t *testing.T) {
33 ctx := context.Background()
34 journalPath := func(f *hrFixture) string {
35 return filepath.Join(moves.JournalDir(f.data, p1Host, p1ID), "journal.json")
36 }
37 // rootOnly is a journal with the right hash in a directory only its
38 // owner can read: what a symlink would make root read on the agent's
39 // behalf.
40 rootOnly := func(t *testing.T) string {
41 t.Helper()
42 dir := t.TempDir()
43 g := &hrFixture{data: dir}
44 g.journal(t, p1ID, hrRendered)
45 if err := os.Chmod(filepath.Join(moves.JournalDir(dir, p1Host, p1ID), "journal.json"), 0o600); err != nil {
46 t.Fatal(err)
47 }
48 return moves.JournalDir(dir, p1Host, p1ID)
49 }
50 for _, tc := range []struct {
51 name string
52 prep func(t *testing.T, f *hrFixture)
53 }{
54 {"a symlink to a root-only journal", func(t *testing.T, f *hrFixture) {
55 p := journalPath(f)
56 if err := os.Remove(p); err != nil {
57 t.Fatal(err)
58 }
59 if err := os.Symlink(filepath.Join(rootOnly(t), "journal.json"), p); err != nil {
60 t.Fatal(err)
61 }
62 }},
63 {"a symlinked directory on the path", func(t *testing.T, f *hrFixture) {
64 dir := moves.JournalDir(f.data, p1Host, p1ID)
65 if err := os.RemoveAll(dir); err != nil {
66 t.Fatal(err)
67 }
68 if err := os.Symlink(rootOnly(t), dir); err != nil {
69 t.Fatal(err)
70 }
71 }},
72 {"a FIFO", func(t *testing.T, f *hrFixture) {
73 p := journalPath(f)
74 if err := os.Remove(p); err != nil {
75 t.Fatal(err)
76 }
77 if err := unix.Mkfifo(p, 0o644); err != nil {
78 t.Fatal(err)
79 }
80 }},
81 {"a hard link", func(t *testing.T, f *hrFixture) {
82 if err := os.Link(journalPath(f), filepath.Join(t.TempDir(), "other")); err != nil {
83 t.Skipf("no hard link across temp dirs: %v", err)
84 }
85 }},
86 {"an oversized journal", func(t *testing.T, f *hrFixture) {
87 if err := os.WriteFile(journalPath(f), []byte(strings.Repeat(" ", heldRenderJournalMax+1)), 0o644); err != nil {
88 t.Fatal(err)
89 }
90 }},
91 } {
92 t.Run(tc.name, func(t *testing.T) {
93 f := newHRFixture(t)
94 tc.prep(t, f)
95 done := make(chan error, 1)
96 go func() {
97 _, err := f.root.Serve(ctx, hrPeer, f.request(heldReleasePublish))
98 done <- err
99 }()
100 select {
101 case err := <-done:
102 if err == nil {
103 t.Fatal("root published on an unsafe journal")
104 }
105 if errors.Is(err, errHeldReleaseNotYet) {
106 // Not yet would make the agent retry an unsafe file forever
107 // without anyone seeing it; it is a refusal.
108 t.Fatalf("an unsafe journal was read as not yet: %v", err)
109 }
110 case <-time.After(5 * time.Second):
111 t.Fatal("root blocked on the journal")
112 }
113 f.untouched(t)
114 })
115 }
116}
117
118// TestHeldReleaseOverTheRootSocket: the agent's client reaches root's
119// held-release route on the claim-mode instance-control mux, with root's
120// peer authentication; root's refusals come back typed; and a root with no
121// such route (an older build, or one without its providers) and a mux with
122// no claim handler at all both read as unavailable.
123func TestHeldReleaseOverTheRootSocket(t *testing.T) {
124 ctx := context.Background()
125 f := newHRFixture(t)
126 authenticated := 0
127 f.root.Authenticate = func(_ context.Context, conn net.Conn) (StopPeer, error) {
128 if conn == nil {
129 t.Error("no request connection")
130 }
131 authenticated++
132 return hrPeer, nil
133 }
134 sock := serveClaimRootWithHeldRelease(t, ClaimRootWithLaunch{HeldRelease: f.root})
135 client := newHeldReleaseRootClient(sock)
136
137 stale := f.request(heldReleasePublish)
138 stale.Session = 99
139 if _, err := client.HeldRelease(ctx, stale); !errors.Is(err, release.ErrStaleHeldStage) {
140 t.Fatalf("a stale request over the socket: %v", err)
141 }
142 resp, err := client.HeldRelease(ctx, f.request(heldReleasePublish))
143 if err != nil || resp.Outcome != heldReleaseOutcomePublished {
144 t.Fatalf("publish over the socket: %+v %v", resp, err)
145 }
146 if v, _ := release.LiveVersion(f.opt, p1Host); v != "v7" {
147 t.Fatalf("live = %q", v)
148 }
149 if authenticated != 2 {
150 t.Fatalf("authenticated %d of 2 requests", authenticated)
151 }
152
153 f.root.DataDir = t.TempDir()
154 f.set(func(f *hrFixture) { f.claim.Holder.Session = 4 })
155 if _, err := client.HeldRelease(ctx, f.request(heldReleasePublish)); !errors.Is(err, errHeldReleaseNotYet) {
156 t.Fatalf("not yet over the socket: %v", err)
157 }
158
159 f.root.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, errors.New("not the edge") }
160 if _, err := client.HeldRelease(ctx, f.request(heldReleasePublish)); !errors.Is(err, ErrClaimStopIdentity) {
161 t.Fatalf("an unauthenticated peer: %v", err)
162 }
163
164 // A body the protocol cannot carry is refused before authority checks.
165 f.root.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return hrPeer, nil }
166 httpc := &http.Client{Transport: &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
167 return (&net.Dialer{}).DialContext(ctx, "unix", sock)
168 }}}
169 r, err := httpc.Post("http://control/claim/held-release", "application/json", strings.NewReader(`{"schema":"x","path":"/etc/shadow"}`))
170 if err != nil {
171 t.Fatal(err)
172 }
173 r.Body.Close()
174 if r.StatusCode != http.StatusBadRequest {
175 t.Fatalf("an unknown field: status %d", r.StatusCode)
176 }
177
178 for name, h := range map[string]http.Handler{
179 "a root without the route": ClaimRootWithLaunch{},
180 "a claim root of old": http.NotFoundHandler(),
181 } {
182 t.Run(name, func(t *testing.T) {
183 old := newHeldReleaseRootClient(serveClaimRoot(t, h))
184 if _, err := old.HeldRelease(ctx, f.request(heldReleaseStage)); !errors.Is(err, errHeldReleaseUnavailable) {
185 t.Fatalf("an old root: %v, want errHeldReleaseUnavailable", err)
186 }
187 })
188 }
189 t.Run("a mux with the claim off", func(t *testing.T) {
190 dir, err := os.MkdirTemp("", "hr")
191 if err != nil {
192 t.Fatal(err)
193 }
194 t.Cleanup(func() { os.RemoveAll(dir) })
195 sock := filepath.Join(dir, "c.sock")
196 ln, err := net.Listen("unix", sock)
197 if err != nil {
198 t.Fatal(err)
199 }
200 srv := &http.Server{Handler: http.NewServeMux()}
201 go srv.Serve(ln)
202 t.Cleanup(func() { srv.Close() })
203 if _, err := newHeldReleaseRootClient(sock).HeldRelease(ctx, f.request(heldReleaseStage)); !errors.Is(err, errHeldReleaseUnavailable) {
204 t.Fatalf("no claim mux: %v", err)
205 }
206 })
207}
208
209// syncBuffer is a log sink the server goroutine and the test share.
210type syncBuffer struct {
211 mu sync.Mutex
212 b bytes.Buffer
213}
214
215func (s *syncBuffer) Write(p []byte) (int, error) {
216 s.mu.Lock()
217 defer s.mu.Unlock()
218 return s.b.Write(p)
219}
220
221func (s *syncBuffer) String() string {
222 s.mu.Lock()
223 defer s.mu.Unlock()
224 return s.b.String()
225}
226
227// TestHeldReleaseRootLogsOnlyValidatedFields (fix round 2, P3-2): root
228// never logs a request's fields before they pass validation, so the
229// authenticated agent cannot forge lines in instance-control's journal;
230// after validation it logs them quoted.
231func TestHeldReleaseRootLogsOnlyValidatedFields(t *testing.T) {
232 ctx := context.Background()
233 f := newHRFixture(t)
234 f.root.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return hrPeer, nil }
235 var logs syncBuffer
236 prev := log.Writer()
237 log.SetOutput(&logs)
238 t.Cleanup(func() { log.SetOutput(prev) })
239 client := newHeldReleaseRootClient(serveClaimRootWithHeldRelease(t, ClaimRootWithLaunch{HeldRelease: f.root}))
240
241 forged := f.request(heldReleasePublish)
242 forged.Host = "a.go.example.com\nmoves: FORGED publication of v9"
243 forged.Version = "v7\nmoves: FORGED"
244 if _, err := client.HeldRelease(ctx, forged); err == nil {
245 t.Fatal("a malformed request was accepted")
246 }
247 stale := f.request(heldReleasePublish)
248 stale.Session = 99
249 if _, err := client.HeldRelease(ctx, stale); !errors.Is(err, release.ErrStaleHeldStage) {
250 t.Fatalf("a stale request: %v", err)
251 }
252 out := logs.String()
253 if strings.Contains(out, "FORGED") {
254 t.Fatalf("root logged an unvalidated field:\n%s", out)
255 }
256 if !strings.Contains(out, `"`+p1Host+`"`) {
257 t.Fatalf("root did not log the validated request quoted:\n%s", out)
258 }
259}
260