// Package config loads keyproxy.toml: the listen address, the bearer // auth ref, and the ref map (mpk- -> backend/source/key). The // config holds locations only — never material; keyproxy.toml.example is // tracked, the real file is gitignored. package config import ( "fmt" "os" "regexp" "sort" "strconv" "strings" "git.knownelement.com/ukrrs/mopac-keyproxy/internal/backend" ) // DefaultListen is the loopback bind address used when [listen] is unset. const DefaultListen = "127.0.0.1:8082" // knownBackends are the backend names the loader accepts. var knownBackends = map[string]bool{ "file": true, "env": true, "bitwarden": true, "vault": true, } // refPattern is the shape every consumer-facing ref must have // (mpk-, lowercase/digits/hyphens). Because the pattern is // enforced before any echo, log lines and error bodies can name refs // safely: an mpk- ref is a placeholder, by construction not material. var refPattern = regexp.MustCompile(`^mpk-[a-z0-9][a-z0-9-]*$`) // ValidRef reports whether name is a well-formed mpk- ref. func ValidRef(name string) bool { return refPattern.MatchString(name) } // Config is the parsed keyproxy.toml. type Config struct { Listen string // bind address, default 127.0.0.1:8082 AuthTokenRef string // [auth] token_ref; must be a file-backend ref Refs map[string]backend.Ref // ref name -> backend ref RefNames []string // sorted ref names } // Load reads, parses and validates the config at path. func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config: %w", err) } doc, err := parseTOML(data) if err != nil { return nil, fmt.Errorf("%s: %w", path, err) } cfg := &Config{Refs: map[string]backend.Ref{}} // Strict surface: unknown keys fail loudly (a misconfigured file must // not start on typo'd best effort). for _, k := range doc.Keys() { switch k { case "listen", "auth", "refs": default: return nil, fmt.Errorf("%s: unknown top-level key %q (allowed: listen, auth, refs)", path, k) } } if s, ok := doc.Str("listen"); ok { cfg.Listen = s } else { cfg.Listen = DefaultListen } auth := doc.Table("auth") for _, k := range auth.Keys() { if k != "token_ref" { return nil, fmt.Errorf("%s: [auth]: unknown key %q (allowed: token_ref)", path, k) } } cfg.AuthTokenRef, _ = auth.Str("token_ref") if cfg.AuthTokenRef == "" { return nil, fmt.Errorf("[auth]: token_ref is required (bearer token for /v1/resolve, resolved through the file backend)") } refs := doc.Table("refs") if len(refs) == 0 { return nil, fmt.Errorf("[refs]: no refs configured") } for _, name := range refs.Keys() { ref, err := parseRef(name, refs.Table(name)) if err != nil { return nil, err } cfg.Refs[name] = ref cfg.RefNames = append(cfg.RefNames, name) } sort.Strings(cfg.RefNames) authRef, ok := cfg.Refs[cfg.AuthTokenRef] if !ok { return nil, fmt.Errorf("[auth]: token_ref %q is not a configured ref", cfg.AuthTokenRef) } if authRef.Backend != "file" { return nil, fmt.Errorf("[auth]: token_ref %q must use the file backend (auth is bootstrapped from a 0600 env file), got %q", cfg.AuthTokenRef, authRef.Backend) } return cfg, nil } // parseRef validates one [refs."mpk-"] table. func parseRef(name string, t Doc) (backend.Ref, error) { if !ValidRef(name) { return backend.Ref{}, fmt.Errorf(`[refs."%s"]: ref name must match mpk- (lowercase alphanumerics and hyphens)`, name) } ref := backend.Ref{Name: name} ref.Backend, _ = t.Str("backend") if !knownBackends[ref.Backend] { return backend.Ref{}, fmt.Errorf(`[refs."%s"]: backend must be one of file, env, bitwarden, vault`, name) } ref.Source, _ = t.Str("source") ref.Key, _ = t.Str("key") ref.Mode, _ = t.Str("mode") if ref.Mode == "" { ref.Mode = "0600" } for _, k := range t.Keys() { switch k { case "backend", "source", "key", "mode": default: return backend.Ref{}, fmt.Errorf(`[refs."%s"]: unknown key %q (allowed: backend, source, key, mode)`, name, k) } } switch ref.Backend { case "file": if ref.Source == "" { return backend.Ref{}, fmt.Errorf(`[refs."%s"]: file backend requires source (path to a 0600 env file)`, name) } if ref.Key == "" { return backend.Ref{}, fmt.Errorf(`[refs."%s"]: file backend requires key (KEY inside the env file)`, name) } if m, err := strconv.ParseUint(strings.TrimPrefix(ref.Mode, "0o"), 8, 32); err != nil || os.FileMode(m)&os.ModePerm == 0 { return backend.Ref{}, fmt.Errorf(`[refs."%s"]: mode must be an octal permission mask like "0600"`, name) } case "env": if ref.Source == "" { return backend.Ref{}, fmt.Errorf(`[refs."%s"]: env backend requires source (environment variable name)`, name) } if ref.Key != "" { return backend.Ref{}, fmt.Errorf(`[refs."%s"]: env backend must not set key (source IS the variable name)`, name) } if _, ok := t.Str("mode"); ok { return backend.Ref{}, fmt.Errorf(`[refs."%s"]: mode applies to the file backend only`, name) } } return ref, nil }