carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostfence
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "github.com/carlosframework/platform/internal/awsinv"
8 "github.com/carlosframework/platform/internal/hostactors"
9 "github.com/carlosframework/platform/internal/hostfloor"
10 "github.com/carlosframework/platform/internal/release"
11 "github.com/carlosframework/platform/internal/termfence"
12 "time"
13)
14
15type Worker = termfence.WrittenBy
16type Termination = termfence.Record
17type EvidenceRef struct {
18 Enrollment string `json:"enrollment"`
19 Key string `json:"key"`
20 SHA256 string `json:"sha256"`
21}
22type TerminationWorker struct {
23 Fences release.Store
24 Enabled bool
25 Directory hostactors.Directory
26 Self Worker
27 Reader func(context.Context, string) (awsinv.ExactEC2API, error)
28 Now func() time.Time
29}
30
31func FenceKey(enrollment string) string { return "control/box-fences/" + enrollment + ".json" }
32func (w TerminationWorker) binding(ctx context.Context, box string) (hostactors.Box, error) {
33 if !w.Enabled || w.Now == nil || w.Fences == nil {
34 return hostactors.Box{}, ErrObservation
35 }
36 o, err := w.Directory.Read(ctx)
37 if err != nil {
38 return hostactors.Box{}, err
39 }
40 self, ok := o.Roster.Boxes[w.Self.Box]
41 if !ok || self.State != "active" || self.Enrollment != w.Self.Enrollment {
42 return hostactors.Box{}, ErrObservation
43 }
44 eligible, err := w.Directory.Lookup(ctx, w.Self.Box)
45 if err != nil || eligible.State != "active" || eligible.Enrollment != w.Self.Enrollment {
46 return hostactors.Box{}, ErrObservation
47 }
48 target, ok := o.Roster.Boxes[box]
49 if !ok || target.Region != self.Region || target.Cloud.Region != self.Cloud.Region || target.Cloud.Account != self.Cloud.Account {
50 return hostactors.Box{}, ErrObservation
51 }
52 return target, nil
53}
54func (w TerminationWorker) Describe(ctx context.Context, box string) (EvidenceRef, error) {
55 b, err := w.binding(ctx, box)
56 if err != nil {
57 return EvidenceRef{}, err
58 }
59 if w.Reader == nil {
60 return EvidenceRef{}, ErrObservation
61 }
62 api, err := w.Reader(ctx, b.Cloud.Region)
63 if err != nil {
64 return EvidenceRef{}, err
65 }
66 out, err := termfence.DescribeInstance(ctx, api, b.Cloud)
67 if err != nil {
68 return EvidenceRef{}, err
69 }
70 return w.verifyAndPersist(ctx, box, b, termfence.FromDescribe(out, b.Cloud.InstanceID, b.Cloud.Region))
71}
72
73func (w TerminationWorker) verifyAndPersist(ctx context.Context, box string, b hostactors.Box, observation termfence.Observation) (EvidenceRef, error) {
74 o, err := w.Directory.Read(ctx)
75 if err != nil {
76 return EvidenceRef{}, err
77 }
78 self, ok := o.Roster.Boxes[w.Self.Box]
79 if !ok || self.State != "active" || self.Enrollment != w.Self.Enrollment {
80 return EvidenceRef{}, ErrObservation
81 }
82 f, err := termfence.Verify(termfence.Box{Ref: box, Enrollment: b.Enrollment, Cloud: b.Cloud}, termfence.Box{Ref: w.Self.Box, Enrollment: w.Self.Enrollment, Cloud: self.Cloud}, observation, w.Now())
83 if err != nil {
84 return EvidenceRef{}, err
85 }
86 f.ObservedAt = w.Now().UTC().Format(time.RFC3339Nano)
87 if err := hostactors.ValidateTerminationFence(o, box, f, w.Now()); err != nil {
88 return EvidenceRef{}, err
89 }
90 return w.persist(ctx, f)
91}
92
93// Event is called only by the regional queue consumer, after the existing
94// queue URL and rule-ARN policy validation. It is never an HTTP request body.
95func (w TerminationWorker) Event(ctx context.Context, box string, raw []byte) (EvidenceRef, error) {
96 b, err := w.binding(ctx, box)
97 if err != nil {
98 return EvidenceRef{}, err
99 }
100 observation, err := termfence.FromEvent(raw)
101 if err != nil {
102 return EvidenceRef{}, err
103 }
104 var event struct {
105 Time time.Time `json:"time"`
106 }
107 if json.Unmarshal(raw, &event) != nil || event.Time.IsZero() || event.Time.After(w.Now().Add(5*time.Second)) {
108 return EvidenceRef{}, ErrObservation
109 }
110 return w.verifyAndPersist(ctx, box, b, observation)
111}
112
113func (w TerminationWorker) persist(ctx context.Context, f Termination) (EvidenceRef, error) {
114 raw, err := json.Marshal(f)
115 if err != nil {
116 return EvidenceRef{}, err
117 }
118 key := FenceKey(f.Enrollment)
119 _, err = w.Fences.Put(ctx, key, raw, "")
120 if err != nil {
121 saved, _, readErr := w.Fences.Get(ctx, key)
122 if readErr != nil {
123 return EvidenceRef{}, errors.Join(err, readErr)
124 }
125 var old Termination
126 o, readErr := w.Directory.Read(ctx)
127 if readErr != nil {
128 return EvidenceRef{}, readErr
129 }
130 if json.Unmarshal(saved, &old) != nil || old.Enrollment != f.Enrollment || old.Cloud != f.Cloud || w.Directory.ValidateTerminationFenceAuthority(ctx, o, f.Box, old, w.Now()) != nil {
131 return EvidenceRef{}, ErrObservation
132 }
133 raw = saved
134 }
135 return EvidenceRef{Enrollment: f.Enrollment, Key: key, SHA256: hostfloor.Digest(raw)}, nil
136}
137