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 "context"
5 "crypto/ecdh"
6 "errors"
7 "log"
8 "os"
9 "strings"
10 "sync"
11
12 "github.com/carlosframework/platform/internal/activator"
13 "github.com/carlosframework/platform/internal/appconfig"
14 "github.com/carlosframework/platform/internal/instancekey"
15 "github.com/carlosframework/platform/internal/registry"
16 "github.com/carlosframework/platform/internal/release"
17)
18
19// replicaHomeKeysPath is the box's list of verified home keys for its
20// replica rows (the list the plan calls CARLOS_REPLICA_HOME_KEYS):
21// "replica-host=sha256" entries, separated by commas or newlines. Only the
22// pre-roll check writes it, as root, replacing it whole (write a temporary
23// file, then rename it into place); the agent only reads it.
24const replicaHomeKeysPath = "/etc/carlos/replica-home-keys"
25
26// instanceKeyReaders are the two readers every ExecBackend that launches in
27// production is given: the box's verified home keys for its replica rows
28// (rulings 26 and 30) and the bucket's configured key (rulings 5 and 27).
29// The agent's backend and the claim path's backend (claimExecBackend) are
30// wired from the same pair, so neither can gate a replica or mint a key
31// by a rule the other lacks (final review I1).
32type instanceKeyReaders struct {
33 HomeKeys func(host string) (sha256 string, ok bool)
34 Configured func(context.Context, registry.Route) (sha256 string, err error)
35}
36
37// newInstanceKeyReaders builds the pair from a box: getenv for the secrets
38// key and the flags, open for the deploy store, list for the home-keys
39// file.
40func newInstanceKeyReaders(getenv func(string) string, open func(context.Context) (release.Store, error), list func() ([]byte, error)) instanceKeyReaders {
41 return instanceKeyReaders{HomeKeys: replicaHomeKeys(list), Configured: configuredKey(getenv, open)}
42}
43
44// productionInstanceKeyReaders reads this box: its environment, its deploy
45// store and /etc/carlos/replica-home-keys.
46func productionInstanceKeyReaders() instanceKeyReaders {
47 return newInstanceKeyReaders(os.Getenv, openDeployStore, func() ([]byte, error) { return os.ReadFile(replicaHomeKeysPath) })
48}
49
50// wire gives eb both readers.
51func (k instanceKeyReaders) wire(eb *activator.ExecBackend) {
52 eb.ReplicaHomeKeys = k.HomeKeys
53 eb.ConfiguredKey = k.Configured
54}
55
56// replicaHomeKeys is ExecBackend.ReplicaHomeKeys (instance-key plan rulings
57// 26, 27 and 30): it reads the list again at every call, so an entry the
58// procedure removes or replaces takes effect at the next replica launch,
59// with no agent restart and no cached copy to go stale. Nothing refreshes
60// the list from a replica's own configuration. An absent list verifies
61// nothing. A list that cannot be read or parsed is logged by the error's
62// position and reason, never its text, and verifies nothing, so every
63// replica launch that would be handed a key is refused until it is fixed.
64// Its log lines name the file by replicaHomeKeysPath, the list it reads in
65// production, so an operator knows what to fix.
66func replicaHomeKeys(read func() ([]byte, error)) func(host string) (string, bool) {
67 return func(host string) (string, bool) {
68 raw, err := read()
69 if errors.Is(err, os.ErrNotExist) {
70 return "", false
71 }
72 if err != nil {
73 log.Printf("agent: replica home keys: %s could not be read (no replica home key verifies): %v", replicaHomeKeysPath, err)
74 return "", false
75 }
76 keys, err := instancekey.ParseHomeKeys(strings.ReplaceAll(string(raw), "\n", ","))
77 if err != nil {
78 log.Printf("agent: replica home keys: %s: %v (no replica home key verifies until it is fixed)", replicaHomeKeysPath, err)
79 return "", false
80 }
81 sha, ok := keys[host]
82 return sha, ok
83 }
84}
85
86// configuredKey is ExecBackend.ConfiguredKey (rulings 5 and 27): the
87// digest of the key rt's configuration in the bucket renders now, read
88// through the deploy store with the box's secrets key. Which boxes have a
89// configuration to read is decided as the config guard decides whether a
90// box can provide a route's config at all (configguard.go): a box with no
91// deployment store and CARLOS_NO_SEALED_CONFIG set has no configuration
92// source and renders no key; one with no store and no such flag is an
93// error; one with a store and no secrets key renders what it can, and a
94// sealed value fails the render here as it fails the guard. The store is
95// opened on first use and kept once open; a failed open is returned, so
96// the mint is deferred or the replica refused, and is tried again at the
97// next launch.
98func configuredKey(getenv func(string) string, open func(context.Context) (release.Store, error)) func(context.Context, registry.Route) (string, error) {
99 var (
100 mu sync.Mutex
101 store release.Store
102 priv *ecdh.PrivateKey
103 )
104 return func(ctx context.Context, rt registry.Route) (string, error) {
105 mu.Lock()
106 if store == nil {
107 var k *ecdh.PrivateKey
108 if raw := getenv("CARLOS_SECRETS_KEY"); raw != "" {
109 var err error
110 k, err = appconfig.ParsePrivateKey(raw, "CARLOS_SECRETS_KEY")
111 if err != nil {
112 mu.Unlock()
113 return "", err
114 }
115 }
116 st, err := open(ctx)
117 if errors.Is(err, errNoDeployStore) && noSealedConfigFlag(getenv) {
118 mu.Unlock()
119 return "", nil // no configuration source: nothing is on its way
120 }
121 if err != nil {
122 mu.Unlock()
123 return "", err
124 }
125 store, priv = st, k
126 }
127 st, k := store, priv
128 mu.Unlock()
129 return appconfig.ConfiguredKeyDigest(ctx, st, k, rt)
130 }
131}
132