carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Rendering a Manifest into the KEY=value lines carlos-app@.service's
2// EnvironmentFile directive reads.
3package appconfig
4
5import (
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.
23func 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).
37func 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