aboutsummaryrefslogtreecommitdiff
path: root/pkg/atlas/config.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/atlas/config.go')
-rw-r--r--pkg/atlas/config.go152
1 files changed, 138 insertions, 14 deletions
diff --git a/pkg/atlas/config.go b/pkg/atlas/config.go
index af70817..8e68bb4 100644
--- a/pkg/atlas/config.go
+++ b/pkg/atlas/config.go
@@ -2,9 +2,13 @@
package atlas
import (
+ "bytes"
+ "errors"
"fmt"
"io/fs"
+ "os"
"os/user"
+ "path/filepath"
"reflect"
"runtime"
"strconv"
@@ -39,8 +43,10 @@ type Config struct {
// Comma-separated list of paths to SSL server certificates to use for SSL.
// The .crt and .key extensions will be appended automatically. If not
- // provided, SSL is disabled.
- ServerCerts []string `env:"ATLAS_SERVER_CERTS"`
+ // provided, SSL is disabled. If a path begins with @, it is treated as a
+ // systemd credential name (i.e., @mycert expands to
+ // $CREDENTIALS_DIRECTORY/mycert.{crt,key}).
+ ServerCerts []string `env:"ATLAS_SERVER_CERTS" sdcreds:"expand,list"`
// Comma-separated list of paths to SSL CA certificates to use for SSL
// client authentication. No effect is ServerCerts is not provided. If not
@@ -117,8 +123,9 @@ type Config struct {
// Experimental option to use deterministic server ID generation based on
// the provided secret and the server info. The secret is used to prevent
- // brute-forcing server IDs from the ID and known server info.
- API0_ServerList_ExperimentalDeterministicServerIDSecret string `env:"ATLAS_API0_SERVERLIST_EXPERIMENTAL_DETERMINISTIC_SERVER_ID_SECRET"`
+ // brute-forcing server IDs from the ID and known server info. If it begins
+ // with @, it is treated as the name of a systemd credential to load.
+ API0_ServerList_ExperimentalDeterministicServerIDSecret string `env:"ATLAS_API0_SERVERLIST_EXPERIMENTAL_DETERMINISTIC_SERVER_ID_SECRET" sdcreds:"load,trimspace"`
// The storage to use for accounts:
// - memory
@@ -135,15 +142,18 @@ type Config struct {
// - file:/path/to/mainmenupromos.json
API0_MainMenuPromos string `env:"ATLAS_API0_MAINMENUPROMOS=none"`
- // The email address to use for Origin login. If not provided, usernames are not
- // resolved during authentication.
- OriginEmail string `env:"ATLAS_ORIGIN_EMAIL"`
+ // The email address to use for Origin login. If not provided, usernames are
+ // not resolved during authentication. If it begins with @, it is treated
+ // as the name of a systemd credential to load.
+ OriginEmail string `env:"ATLAS_ORIGIN_EMAIL" sdcreds:"load,trimspace"`
- // The password for Origin login.
- OriginPassword string `env:"ATLAS_ORIGIN_PASSWORD"`
+ // The password for Origin login. If it begins with @, it is treated as the
+ // name of a systemd credential to load.
+ OriginPassword string `env:"ATLAS_ORIGIN_PASSWORD" sdcreds:"load,trimspace"`
- // The base32 TOTP secret for Origin login.
- OriginTOTP string `env:"ATLAS_ORIGIN_TOTP"`
+ // The base32 TOTP secret for Origin login. If it begins with @, it is
+ // treated as the name of a systemd credential to load.
+ OriginTOTP string `env:"ATLAS_ORIGIN_TOTP" sdcreds:"load,trimspace"`
// OriginHARGzip controls whether to compress saved HAR archives.
OriginHARGzip bool `env:"ATLAS_ORIGIN_HAR_GZIP"`
@@ -160,13 +170,14 @@ type Config struct {
// restarts. Highly recommended.
OriginPersist string `env:"ATLAS_ORIGIN_PERSIST"`
- // Secret token for accessing internal metrics.
- MetricsSecret string `env:"ATLAS_METRICS_SECRET"`
+ // Secret token for accessing internal metrics. If it begins with @, it is
+ // treated as the name of a systemd credential to load.
+ MetricsSecret string `env:"ATLAS_METRICS_SECRET" sdcreds:"load,trimspace"`
// The path to use for static website files. If a file named redirects.json
// exists, it is read at startup, reloaded on SIGHUP, and used as a mapping
// of top-level names to URLs.
- Web string `env:"ATLAS_WEB="`
+ Web string `env:"ATLAS_WEB"`
// The path to the IP2Location database, which should contain at least the
// country and region fields. The database must not be modified while atlas
@@ -200,6 +211,9 @@ func (c *Config) UnmarshalEnv(es []string, incremental bool) error {
if !ok {
continue
}
+
+ // get the default value, and check if it can be explicitly set to an
+ // empty value
var unsettable bool
key, val, _ := strings.Cut(env, "=")
if strings.HasSuffix(key, "?") {
@@ -207,13 +221,27 @@ func (c *Config) UnmarshalEnv(es []string, incremental bool) error {
unsettable = true
}
if v, exists := em[key]; exists {
+ // expand credentials before attempting to set the var or checking
+ // if it can be set to an empty value
+ v, err := sdcreds(v, ctf.Tag.Get("sdcreds"))
+ if err != nil {
+ return fmt.Errorf("env %s: expand systemd credentials: %w", key, err)
+ }
+
+ // if the value is non-empty or we are allowed to set it to an empty
+ // value, set it, otherwise simply keep the default
if unsettable || v != "" {
val = v
}
+
+ // we're finished processing this var
delete(em, key)
} else if incremental {
+ // if we're only doing incremental updates, don't use the default
+ // value if the current env list doesn't have the var
continue
}
+
switch cvf := cv.FieldByName(ctf.Name); cvf.Interface().(type) {
case string:
cvf.SetString(val)
@@ -333,3 +361,99 @@ func parseUIDGID(s string) (UIDGID, error) {
}
return u, nil
}
+
+// sdcreds expands systemd credentials in v (prefixed by "@") according to tag,
+// which consists of a mode followed by optional flags.
+//
+// Mode:
+// - (none): return the original value
+// - expand: expand to the cred path
+// - load: read the cred contents
+//
+// Args:
+// - trimspace (load): trim leading/trailing whitespace from the cred value
+// - list (expand, load): split v by "," and process each item individually
+func sdcreds(v string, tag string) (string, error) {
+ if tag == "" {
+ return v, nil
+ }
+
+ var mode struct {
+ expand bool
+ load bool
+ }
+ var opts struct {
+ trimspace bool
+ list bool
+ }
+
+ tag, args, _ := strings.Cut(tag, ",")
+ switch tag {
+ case "expand":
+ mode.expand = true
+ case "load":
+ mode.load = true
+ default:
+ return "", fmt.Errorf("invalid struct tag %q", tag)
+ }
+ for _, arg := range strings.Split(args, ",") {
+ switch {
+ case mode.load && arg == "trimspace":
+ opts.trimspace = true
+ case (mode.load || mode.expand) && arg == "list":
+ opts.list = true
+ default:
+ return "", fmt.Errorf("invalid struct tag %q arg %q", tag, arg)
+ }
+ }
+
+ var vs []string
+ if opts.list {
+ vs = strings.Split(v, ",")
+ } else {
+ vs = []string{v}
+ }
+
+ vsi := make([]int, 0, len(vs))
+ for i, x := range vs {
+ if len(x) != 0 && x[0] == '@' {
+ vsi = append(vsi, i)
+ }
+ }
+ if len(vsi) == 0 {
+ return v, nil
+ }
+ if mode.expand || mode.load {
+ crd := os.Getenv("CREDENTIALS_DIRECTORY")
+ if crd == "" {
+ return "", fmt.Errorf("expand %q: systemd CREDENTIALS_DIRECTORY env var not set", v)
+ }
+ if !filepath.IsAbs(crd) {
+ return "", fmt.Errorf("expand %q: systemd CREDENTIALS_DIRECTORY=%q env var is not an absolute path", v, crd)
+ }
+ for _, i := range vsi {
+ cred := vs[i][1:]
+ if strings.Contains(cred, "/") || strings.Contains(cred, string(filepath.Separator)) {
+ return "", fmt.Errorf("expand %q: invalid credential name %q", v, cred)
+ }
+ vs[i] = filepath.Join(crd, cred)
+ }
+ }
+ if mode.load {
+ for _, i := range vsi {
+ pt := vs[i]
+ buf, err := os.ReadFile(pt)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return v, fmt.Errorf("expand %q: no such credential %q", v, filepath.Base(pt))
+ }
+ return v, fmt.Errorf("expand %q: read credential %q: %w", v, filepath.Base(pt), err)
+ }
+ if opts.trimspace {
+ buf = bytes.TrimSpace(buf)
+ }
+ vs[i] = string(buf)
+ }
+ }
+ return strings.Join(vs, ","), nil
+}