carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "fmt"
5 "os"
6 "regexp"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/hostfloor"
11 "github.com/carlosframework/platform/internal/hostops"
12 "github.com/carlosframework/platform/internal/status"
13)
14
15// The receipt database identity also names the EC2 instance the console runs
16// on. A disk image or snapshot keeps the nonce, the filesystem UUID and the
17// inode, so without it a clone could fence or commit the original's SQL
18// effects. Root's enrollment file says which instance this disk was enrolled
19// on, but the image carries that file too, so it is only believed when the
20// running machine says the same; OpenGated refuses a database whose stored
21// instance is another machine's.
22const (
23 sqlHostActorPath = "/etc/carlos/host-actor.json"
24 // On Nitro instances the hypervisor sets the board asset tag to the
25 // instance ID; a disk copy cannot carry it.
26 sqlDMIAssetTagPath = "/sys/devices/virtual/dmi/id/board_asset_tag"
27)
28
29var sqlInstanceIdentity = readSQLInstanceIdentity
30
31// The sources are vars so tests can stand in for /etc, /sys and IMDS.
32var (
33 sqlEnrolledInstance = func() (string, error) {
34 e, err := hostfloor.ReadEnrollment(sqlHostActorPath)
35 if err != nil {
36 return "", err
37 }
38 return e.InstanceID, nil
39 }
40 sqlDMIInstance = func() string {
41 b, err := os.ReadFile(sqlDMIAssetTagPath)
42 if err != nil {
43 return ""
44 }
45 return strings.TrimSpace(string(b))
46 }
47 // status.InstanceID is IMDSv2 bounded to two seconds; it says "" off EC2.
48 sqlIMDSInstance = status.InstanceID
49)
50
51var ec2InstanceID = regexp.MustCompile(`^i-[0-9a-f]{8,17}$`)
52
53// sqlLiveInstance is the running machine's instance ID, or "". The Nitro
54// asset tag is read first because it needs no network; other hardware puts
55// arbitrary text there, so only an instance-shaped tag counts.
56func sqlLiveInstance() string {
57 if id := sqlDMIInstance(); ec2InstanceID.MatchString(id) {
58 return id
59 }
60 return sqlIMDSInstance()
61}
62
63func readSQLInstanceIdentity() (string, error) {
64 enrolled, err := sqlEnrolledInstance()
65 if err != nil {
66 return "", err
67 }
68 live := sqlLiveInstance()
69 if live == "" {
70 return "", fmt.Errorf("%w: cannot read this machine's instance ID", hostops.ErrIdentity)
71 }
72 if live != enrolled {
73 return "", fmt.Errorf("%w: enrolled on instance %q but running on %q", hostops.ErrIdentity, enrolled, live)
74 }
75 return live, nil
76}
77
78// sqlInstance is the validated per-instance component. A ':' would make the
79// database ID ambiguous, so it is refused with an empty one.
80func sqlInstance() (string, error) {
81 id, err := sqlInstanceIdentity()
82 if err != nil {
83 return "", err
84 }
85 if id == "" || strings.ContainsAny(id, ":\n") {
86 return "", hostops.ErrIdentity
87 }
88 return id, nil
89}
90
91func useSQLInstanceIdentity(fn func() (string, error)) (restore func()) {
92 old := sqlInstanceIdentity
93 sqlInstanceIdentity = fn
94 return func() { sqlInstanceIdentity = old }
95}
96
97// UseSQLInstanceIdentityForTest makes OpenGated see id as this machine's
98// instance until restore runs. Other packages' tests open gated databases on
99// hosts with no enrollment file; production binaries panic here.
100func UseSQLInstanceIdentityForTest(id string) (restore func()) {
101 if !testing.Testing() {
102 panic("console: UseSQLInstanceIdentityForTest outside a test binary")
103 }
104 return useSQLInstanceIdentity(func() (string, error) { return id, nil })
105}
106