| 1 | // Rendering a Manifest into the KEY=value lines carlos-app@.service's |
| 2 | // EnvironmentFile directive reads. |
| 3 | package appconfig |
| 4 | |
| 5 | import ( |
| 6 | "crypto/ecdh" |
| 7 | "fmt" |
| 8 | "sort" |
| 9 | "strings" |
| 10 | ) |
| 11 | |
| 12 | // ValidVarValue reports whether val can be materialized into an |
| 13 | // EnvironmentFile verbatim. systemd's parser is shell-like — it interprets |
| 14 | // backslash escapes and single/double quotes, and drops a line outright |
| 15 | // when a quote is unbalanced — so a value carrying any of those would be |
| 16 | // silently corrupted on the host with nothing surfaced to the operator. |
| 17 | // Deliberately a rejection and not a quoting scheme: getting |
| 18 | // systemd-compatible escaping subtly wrong fails in exactly the same |
| 19 | // silent way. One predicate shared by set-time validation (where the |
| 20 | // operator can still see the plaintext and act) and RenderEnv (defense in |
| 21 | // depth for manifests written before the set-time check existed), so the |
| 22 | // two can never diverge. |
| 23 | func ValidVarValue(val string) error { |
| 24 | if strings.ContainsAny(val, "\n\x00\\'\"") { |
| 25 | return fmt.Errorf("value contains a backslash, quote, newline or NUL, none of which are safely representable in an EnvironmentFile") |
| 26 | } |
| 27 | return nil |
| 28 | } |
| 29 | |
| 30 | // RenderEnv decrypts m's secret entries with priv, as the config of bind (the |
| 31 | // route's account and app — see Binding), and renders the whole |
| 32 | // manifest as EnvironmentFile-shaped `KEY=value\n` lines, keys sorted so |
| 33 | // the output is deterministic and diffable against what's already on |
| 34 | // disk. priv may be nil iff m has no "secret" entries (callers that know |
| 35 | // they're only rendering plain env vars, e.g. a future env-only path, |
| 36 | // don't need a key in hand). |
| 37 | func RenderEnv(m *Manifest, priv *ecdh.PrivateKey, bind Binding) ([]byte, error) { |
| 38 | keys := make([]string, 0, len(m.Vars)) |
| 39 | for k := range m.Vars { |
| 40 | keys = append(keys, k) |
| 41 | } |
| 42 | sort.Strings(keys) |
| 43 | var b strings.Builder |
| 44 | for _, k := range keys { |
| 45 | v := m.Vars[k] |
| 46 | var val string |
| 47 | switch v.Kind { |
| 48 | case "env": |
| 49 | val = v.Value |
| 50 | case "secret": |
| 51 | if priv == nil { |
| 52 | return nil, fmt.Errorf("var %s: secret entry but no decryption key was supplied", k) |
| 53 | } |
| 54 | plain, err := Open(priv, bind, v.Envelope) |
| 55 | if err != nil { |
| 56 | return nil, fmt.Errorf("decrypt %s: %w", k, err) |
| 57 | } |
| 58 | val = string(plain) |
| 59 | default: |
| 60 | return nil, fmt.Errorf("var %s: unknown kind %q", k, v.Kind) |
| 61 | } |
| 62 | if err := ValidVarValue(val); err != nil { |
| 63 | return nil, fmt.Errorf("var %s: %w", k, err) |
| 64 | } |
| 65 | fmt.Fprintf(&b, "%s=%s\n", k, val) |
| 66 | } |
| 67 | return []byte(b.String()), nil |
| 68 | } |
| 69 | |