214 lines
7.8 KiB
Go
214 lines
7.8 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Mode string
|
|
|
|
const (
|
|
ModeStandalone Mode = "standalone"
|
|
ModeMaster Mode = "master"
|
|
ModeAgent Mode = "agent"
|
|
)
|
|
|
|
type Config struct {
|
|
Mode Mode
|
|
ListenAddr, BaseURL, DataDir, StacksDir, AppSecret string
|
|
SecureCookies, AuthDisabled bool
|
|
OIDCIssuer, OIDCClientID, OIDCClientSecret, OIDCRedirectURL, OIDCAdminGroup, OIDCOperatorGroup string
|
|
AgentToken, HostRoot, HostAURUser string
|
|
AllowHostUserManagement, AllowHostPermissionManagement bool
|
|
HostSecurityEnabled, AllowHostSecurityChanges, AllowHostPackageManagement bool
|
|
HostSecurityPID int
|
|
CheckConcurrency, RetentionDays, AuditRetentionDays int
|
|
HTTPTimeout time.Duration
|
|
}
|
|
|
|
func Load() (Config, error) {
|
|
checkConcurrency, err := envIntStrict("CHECK_CONCURRENCY", 8)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
retentionDays, err := envIntStrict("CHECK_RETENTION_DAYS", 30)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
auditRetentionDays, err := envIntStrict("AUDIT_RETENTION_DAYS", 180)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
httpTimeoutSeconds, err := envIntStrict("HTTP_TIMEOUT_SECONDS", 10)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
authDisabled, err := envBoolStrict("AUTH_DISABLED", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
allowHostUserManagement, err := envBoolStrict("ALLOW_HOST_USER_MANAGEMENT", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
allowHostPermissionManagement, err := envBoolStrict("ALLOW_HOST_PERMISSION_MANAGEMENT", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
hostSecurityEnabled, err := envBoolStrict("HOST_SECURITY_ENABLED", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
allowHostSecurityChanges, err := envBoolStrict("ALLOW_HOST_SECURITY_CHANGES", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
allowHostPackageManagement, err := envBoolStrict("ALLOW_HOST_PACKAGE_MANAGEMENT", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
hostSecurityPID, err := envIntStrict("HOST_SECURITY_HOST_PID", 1)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
c := Config{
|
|
Mode: Mode(env("APP_MODE", "standalone")),
|
|
ListenAddr: env("LISTEN_ADDR", ":8080"),
|
|
BaseURL: strings.TrimRight(env("BASE_URL", "http://localhost:8080"), "/"),
|
|
DataDir: env("DATA_DIR", "/data"),
|
|
StacksDir: env("STACKS_DIR", "/stacks"),
|
|
AppSecret: os.Getenv("APP_SECRET"),
|
|
AuthDisabled: authDisabled,
|
|
OIDCIssuer: strings.TrimRight(os.Getenv("OIDC_ISSUER"), "/"),
|
|
OIDCClientID: os.Getenv("OIDC_CLIENT_ID"),
|
|
OIDCClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
|
|
OIDCRedirectURL: os.Getenv("OIDC_REDIRECT_URL"),
|
|
OIDCAdminGroup: env("OIDC_ADMIN_GROUP", "dockwatch-admins"),
|
|
OIDCOperatorGroup: env("OIDC_OPERATOR_GROUP", "dockwatch-operators"),
|
|
AgentToken: os.Getenv("AGENT_TOKEN"),
|
|
HostRoot: cleanOptionalPath(os.Getenv("HOST_ROOT")),
|
|
HostAURUser: strings.TrimSpace(os.Getenv("HOST_AUR_USER")),
|
|
AllowHostUserManagement: allowHostUserManagement,
|
|
AllowHostPermissionManagement: allowHostPermissionManagement,
|
|
HostSecurityEnabled: hostSecurityEnabled,
|
|
AllowHostSecurityChanges: allowHostSecurityChanges,
|
|
AllowHostPackageManagement: allowHostPackageManagement,
|
|
HostSecurityPID: hostSecurityPID,
|
|
CheckConcurrency: checkConcurrency,
|
|
RetentionDays: retentionDays,
|
|
AuditRetentionDays: auditRetentionDays,
|
|
HTTPTimeout: time.Duration(httpTimeoutSeconds) * time.Second,
|
|
}
|
|
c.SecureCookies = strings.HasPrefix(c.BaseURL, "https://")
|
|
if c.OIDCRedirectURL == "" {
|
|
c.OIDCRedirectURL = c.BaseURL + "/auth/callback"
|
|
}
|
|
switch c.Mode {
|
|
case ModeStandalone, ModeMaster, ModeAgent:
|
|
default:
|
|
return c, fmt.Errorf("APP_MODE must be standalone, master, or agent")
|
|
}
|
|
if c.CheckConcurrency < 1 || c.CheckConcurrency > 128 {
|
|
return c, fmt.Errorf("CHECK_CONCURRENCY must be between 1 and 128")
|
|
}
|
|
if c.RetentionDays < 0 || c.RetentionDays > 3650 {
|
|
return c, fmt.Errorf("CHECK_RETENTION_DAYS must be between 0 and 3650")
|
|
}
|
|
if c.AuditRetentionDays < 0 || c.AuditRetentionDays > 3650 {
|
|
return c, fmt.Errorf("AUDIT_RETENTION_DAYS must be between 0 and 3650")
|
|
}
|
|
if c.HTTPTimeout < time.Second || c.HTTPTimeout > 5*time.Minute {
|
|
return c, fmt.Errorf("HTTP_TIMEOUT_SECONDS must be between 1 and 300")
|
|
}
|
|
if c.Mode != ModeAgent {
|
|
u, err := url.Parse(c.BaseURL)
|
|
if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
|
|
return c, errors.New("BASE_URL must be an absolute http(s) URL without credentials, query or fragment")
|
|
}
|
|
}
|
|
if c.HostRoot != "" && !filepath.IsAbs(c.HostRoot) {
|
|
return c, errors.New("HOST_ROOT must be an absolute path")
|
|
}
|
|
if c.AllowHostUserManagement && c.HostRoot == "" {
|
|
return c, errors.New("ALLOW_HOST_USER_MANAGEMENT=true requires HOST_ROOT")
|
|
}
|
|
if c.AllowHostPermissionManagement && c.HostRoot == "" {
|
|
return c, errors.New("ALLOW_HOST_PERMISSION_MANAGEMENT=true requires HOST_ROOT")
|
|
}
|
|
if c.HostSecurityEnabled && c.HostRoot == "" {
|
|
return c, errors.New("HOST_SECURITY_ENABLED=true requires HOST_ROOT")
|
|
}
|
|
if c.AllowHostSecurityChanges && !c.HostSecurityEnabled {
|
|
return c, errors.New("ALLOW_HOST_SECURITY_CHANGES=true requires HOST_SECURITY_ENABLED=true")
|
|
}
|
|
if c.AllowHostPackageManagement && !c.AllowHostSecurityChanges {
|
|
return c, errors.New("ALLOW_HOST_PACKAGE_MANAGEMENT=true requires ALLOW_HOST_SECURITY_CHANGES=true")
|
|
}
|
|
if c.HostSecurityPID < 1 {
|
|
return c, errors.New("HOST_SECURITY_HOST_PID must be greater than 0")
|
|
}
|
|
if c.Mode == ModeAgent {
|
|
if len(c.AgentToken) < 24 {
|
|
return c, errors.New("AGENT_TOKEN must be at least 24 characters in agent mode")
|
|
}
|
|
return c, nil
|
|
}
|
|
if len(c.AppSecret) < 32 {
|
|
return c, errors.New("APP_SECRET must be at least 32 characters")
|
|
}
|
|
if !c.AuthDisabled && (c.OIDCIssuer == "" || c.OIDCClientID == "" || c.OIDCClientSecret == "") {
|
|
return c, errors.New("OIDC_ISSUER, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET are required unless AUTH_DISABLED=true")
|
|
}
|
|
return c, nil
|
|
}
|
|
func (c Config) DBPath() string { return c.DataDir + "/dockwatch.db" }
|
|
func (c Config) EncryptionKey() []byte { s := sha256.Sum256([]byte(c.AppSecret)); return s[:] }
|
|
func (c Config) SecretFingerprint() string {
|
|
h := sha256.Sum256([]byte(c.AppSecret))
|
|
return base64.RawURLEncoding.EncodeToString(h[:6])
|
|
}
|
|
func cleanOptionalPath(v string) string {
|
|
v = strings.TrimSpace(v)
|
|
if v == "" {
|
|
return ""
|
|
}
|
|
return filepath.Clean(v)
|
|
}
|
|
|
|
func env(k, f string) string {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
return f
|
|
}
|
|
func envIntStrict(k string, f int) (int, error) {
|
|
v := strings.TrimSpace(os.Getenv(k))
|
|
if v == "" {
|
|
return f, nil
|
|
}
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s must be an integer: %w", k, err)
|
|
}
|
|
return n, nil
|
|
}
|
|
func envBoolStrict(k string, f bool) (bool, error) {
|
|
v := strings.TrimSpace(os.Getenv(k))
|
|
if v == "" {
|
|
return f, nil
|
|
}
|
|
b, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s must be a boolean: %w", k, err)
|
|
}
|
|
return b, nil
|
|
}
|