// Rendering a Manifest into the KEY=value lines carlos-app@.service's // EnvironmentFile directive reads. package appconfig import ( "crypto/ecdh" "fmt" "sort" "strings" ) // ValidVarValue reports whether val can be materialized into an // EnvironmentFile verbatim. systemd's parser is shell-like — it interprets // backslash escapes and single/double quotes, and drops a line outright // when a quote is unbalanced — so a value carrying any of those would be // silently corrupted on the host with nothing surfaced to the operator. // Deliberately a rejection and not a quoting scheme: getting // systemd-compatible escaping subtly wrong fails in exactly the same // silent way. One predicate shared by set-time validation (where the // operator can still see the plaintext and act) and RenderEnv (defense in // depth for manifests written before the set-time check existed), so the // two can never diverge. func ValidVarValue(val string) error { if strings.ContainsAny(val, "\n\x00\\'\"") { return fmt.Errorf("value contains a backslash, quote, newline or NUL, none of which are safely representable in an EnvironmentFile") } return nil } // RenderEnv decrypts m's secret entries with priv, as the config of bind (the // route's account and app — see Binding), and renders the whole // manifest as EnvironmentFile-shaped `KEY=value\n` lines, keys sorted so // the output is deterministic and diffable against what's already on // disk. priv may be nil iff m has no "secret" entries (callers that know // they're only rendering plain env vars, e.g. a future env-only path, // don't need a key in hand). func RenderEnv(m *Manifest, priv *ecdh.PrivateKey, bind Binding) ([]byte, error) { keys := make([]string, 0, len(m.Vars)) for k := range m.Vars { keys = append(keys, k) } sort.Strings(keys) var b strings.Builder for _, k := range keys { v := m.Vars[k] var val string switch v.Kind { case "env": val = v.Value case "secret": if priv == nil { return nil, fmt.Errorf("var %s: secret entry but no decryption key was supplied", k) } plain, err := Open(priv, bind, v.Envelope) if err != nil { return nil, fmt.Errorf("decrypt %s: %w", k, err) } val = string(plain) default: return nil, fmt.Errorf("var %s: unknown kind %q", k, v.Kind) } if err := ValidVarValue(val); err != nil { return nil, fmt.Errorf("var %s: %w", k, err) } fmt.Fprintf(&b, "%s=%s\n", k, val) } return []byte(b.String()), nil }