From f9160a768694dfdadfc72bb6b289345f73445502 Mon Sep 17 00:00:00 2001 From: TronoSfera <119615520+TronoSfera@users.noreply.github.com> Date: Mon, 18 May 2026 14:17:54 +0300 Subject: [PATCH] fix(agent): env vars BACKUPY_* and accept 64-hex agent keys Two defects exposed by the first real-world deployment (Flw VPS): - Env tags were BACKUP_* (no Y). Server + dashboard use BACKUPY_*. - agentKeyPattern only matched bkpy_(live|test)_<32 alnum>; server's generateAgentKey emits 64 lowercase hex chars. Accept both. --- apps/agent/README.md | 18 ++++++------- apps/agent/cmd/agent/healthcheck.go | 2 +- apps/agent/internal/config/config.go | 32 +++++++++++------------ apps/agent/internal/config/config_test.go | 2 +- apps/agent/internal/logging/logging.go | 2 +- apps/agent/internal/state/crypto.go | 4 +-- apps/agent/internal/state/state.go | 4 +-- apps/agent/internal/wss/client.go | 2 +- docs/03-agent-spec.md | 16 ++++++------ 9 files changed, 41 insertions(+), 41 deletions(-) diff --git a/apps/agent/README.md b/apps/agent/README.md index 1bffd6f..7872543 100644 --- a/apps/agent/README.md +++ b/apps/agent/README.md @@ -31,8 +31,8 @@ services: backup-agent: image: backupy/agent:1 environment: - BACKUP_SERVER_URL: https://api.backupy.ru - BACKUP_AGENT_KEY: ${BACKUP_KEY} + BACKUPY_SERVER_URL: https://api.backupy.ru + BACKUPY_AGENT_KEY: ${BACKUP_KEY} volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - backup-agent-state:/var/lib/backup-agent @@ -46,12 +46,12 @@ volumes: | Var | Required | Default | Notes | |---|---|---|---| -| `BACKUP_SERVER_URL` | yes | `https://api.backupy.ru` | Must be `https://` (override with `BACKUP_DEV_ALLOW_INSECURE=true` for local dev). | -| `BACKUP_AGENT_KEY` | yes | — | Format `bkpy_(live\|test)_<32 alnum>`. Never logged. | -| `BACKUP_STATE_DIR` | no | `/var/lib/backup-agent` | Volume-mounted. Must be writable by uid 65532 (distroless `nonroot`). | -| `BACKUP_LOG_LEVEL` | no | `info` | `trace`/`debug`/`info`/`warn`/`error`. | -| `BACKUP_DOCKER_SOCKET` | no | `/var/run/docker.sock` | Mounted read-only. | -| `BACKUP_DEV_ALLOW_INSECURE` | no | `false` | Allows `http://` server URL — dev only. | +| `BACKUPY_SERVER_URL` | yes | `https://api.backupy.ru` | Must be `https://` (override with `BACKUPY_DEV_ALLOW_INSECURE=true` for local dev). | +| `BACKUPY_AGENT_KEY` | yes | — | Format `bkpy_(live\|test)_<32 alnum>`. Never logged. | +| `BACKUPY_STATE_DIR` | no | `/var/lib/backup-agent` | Volume-mounted. Must be writable by uid 65532 (distroless `nonroot`). | +| `BACKUPY_LOG_LEVEL` | no | `info` | `trace`/`debug`/`info`/`warn`/`error`. | +| `BACKUPY_DOCKER_SOCKET` | no | `/var/run/docker.sock` | Mounted read-only. | +| `BACKUPY_DEV_ALLOW_INSECURE` | no | `false` | Allows `http://` server URL — dev only. | Everything else (targets, schedules, retention, S3 creds, hooks) comes from the server via `ConfigUpdate` after registration. @@ -127,7 +127,7 @@ Packages that require generated proto code to compile: ## Security notes - TLS 1.3 to all server endpoints (enforced by `coder/websocket` defaults). -- `BACKUP_AGENT_KEY` is never logged (`slog` ReplaceAttr redacts known keys +- `BACKUPY_AGENT_KEY` is never logged (`slog` ReplaceAttr redacts known keys defensively; the value is also `json:"-"` in `Config`). - State at rest is AES-256-GCM keyed by HKDF-SHA256 of the agent key. - Docker socket is mounted read-only. diff --git a/apps/agent/cmd/agent/healthcheck.go b/apps/agent/cmd/agent/healthcheck.go index 233fa8e..2bd7577 100644 --- a/apps/agent/cmd/agent/healthcheck.go +++ b/apps/agent/cmd/agent/healthcheck.go @@ -16,7 +16,7 @@ import ( // // Health criteria: // -// 1. Required env vars are set (BACKUP_AGENT_KEY / BACKUP_SERVER_URL). +// 1. Required env vars are set (BACKUPY_AGENT_KEY / BACKUPY_SERVER_URL). // 2. The state.db file can be opened (validates encryption key + on-disk // integrity). // diff --git a/apps/agent/internal/config/config.go b/apps/agent/internal/config/config.go index 625db1e..4e768f3 100644 --- a/apps/agent/internal/config/config.go +++ b/apps/agent/internal/config/config.go @@ -20,29 +20,29 @@ import ( "github.com/caarlos0/env/v11" ) -// agentKeyPattern enforces the documented BACKUP_AGENT_KEY format +// agentKeyPattern enforces the documented BACKUPY_AGENT_KEY format // `bkpy_(live|test)_<32 base62 chars>`. The server issues keys in // this exact shape — see docs/03-agent-spec.md and server task A-09. -var agentKeyPattern = regexp.MustCompile(`^bkpy_(live|test)_[A-Za-z0-9]{32}$`) +var agentKeyPattern = regexp.MustCompile(`^(?:bkpy_(?:live|test)_[A-Za-z0-9]{32}|[a-f0-9]{64})$`) // Config holds all agent bootstrap configuration. type Config struct { - ServerURL string `env:"BACKUP_SERVER_URL,required" envDefault:"https://api.backupy.ru"` - AgentKey string `env:"BACKUP_AGENT_KEY,required" json:"-"` - StateDir string `env:"BACKUP_STATE_DIR" envDefault:"/var/lib/backup-agent"` - LogLevel string `env:"BACKUP_LOG_LEVEL" envDefault:"info"` - DockerSocket string `env:"BACKUP_DOCKER_SOCKET" envDefault:"/var/run/docker.sock"` + ServerURL string `env:"BACKUPY_SERVER_URL,required" envDefault:"https://api.backupy.ru"` + AgentKey string `env:"BACKUPY_AGENT_KEY,required" json:"-"` + StateDir string `env:"BACKUPY_STATE_DIR" envDefault:"/var/lib/backup-agent"` + LogLevel string `env:"BACKUPY_LOG_LEVEL" envDefault:"info"` + DockerSocket string `env:"BACKUPY_DOCKER_SOCKET" envDefault:"/var/run/docker.sock"` // DevAllowInsecure relaxes the https:// requirement on ServerURL. // Intended for local development against a plaintext server only. - DevAllowInsecure bool `env:"BACKUP_DEV_ALLOW_INSECURE" envDefault:"false"` + DevAllowInsecure bool `env:"BACKUPY_DEV_ALLOW_INSECURE" envDefault:"false"` // MetricsListenAddr is the bind address for the Prometheus // `/metrics` endpoint (D-19). Default is loopback only — // 127.0.0.1:9090. Set to empty to disable the metrics server. // SECURITY: never bind to 0.0.0.0 in production; the endpoint // reveals job IDs and run cadence usable for host fingerprinting. - MetricsListenAddr string `env:"BACKUP_METRICS_LISTEN_ADDR" envDefault:"127.0.0.1:9090"` + MetricsListenAddr string `env:"BACKUPY_METRICS_LISTEN_ADDR" envDefault:"127.0.0.1:9090"` } // Load parses environment variables into a Config and validates them. @@ -62,7 +62,7 @@ func Load() (*Config, error) { // Validate enforces the documented constraints on each field. // // - ServerURL must parse as an https:// URL (http:// only with -// BACKUP_DEV_ALLOW_INSECURE=true). +// BACKUPY_DEV_ALLOW_INSECURE=true). // - AgentKey must match the canonical `bkpy_(live|test)_…` pattern. // - StateDir must be writable; we test by creating and removing a temp // file so a misconfigured volume mount fails fast at startup. @@ -71,7 +71,7 @@ func (c *Config) Validate() error { return err } if !agentKeyPattern.MatchString(c.AgentKey) { - return errors.New("config: BACKUP_AGENT_KEY has invalid format; expected bkpy_(live|test)_<32 alnum>") + return errors.New("config: BACKUPY_AGENT_KEY has invalid format; expected 64 hex chars (or legacy bkpy_(live|test)_<32 alnum>)") } if err := validateStateDirWritable(c.StateDir); err != nil { return err @@ -82,27 +82,27 @@ func (c *Config) Validate() error { func validateServerURL(raw string, allowInsecure bool) error { u, err := url.Parse(raw) if err != nil { - return fmt.Errorf("config: BACKUP_SERVER_URL is not a valid URL: %w", err) + return fmt.Errorf("config: BACKUPY_SERVER_URL is not a valid URL: %w", err) } if u.Host == "" { - return errors.New("config: BACKUP_SERVER_URL is missing host") + return errors.New("config: BACKUPY_SERVER_URL is missing host") } switch u.Scheme { case "https": return nil case "http": if !allowInsecure { - return errors.New("config: BACKUP_SERVER_URL must use https:// (set BACKUP_DEV_ALLOW_INSECURE=true for local dev)") + return errors.New("config: BACKUPY_SERVER_URL must use https:// (set BACKUPY_DEV_ALLOW_INSECURE=true for local dev)") } return nil default: - return fmt.Errorf("config: BACKUP_SERVER_URL has unsupported scheme %q (expected https)", u.Scheme) + return fmt.Errorf("config: BACKUPY_SERVER_URL has unsupported scheme %q (expected https)", u.Scheme) } } func validateStateDirWritable(dir string) error { if dir == "" { - return errors.New("config: BACKUP_STATE_DIR must not be empty") + return errors.New("config: BACKUPY_STATE_DIR must not be empty") } // Ensure the directory exists; create it (and parents) if missing. // 0o700 — only the agent UID should ever touch state. diff --git a/apps/agent/internal/config/config_test.go b/apps/agent/internal/config/config_test.go index 430e236..a7e49f7 100644 --- a/apps/agent/internal/config/config_test.go +++ b/apps/agent/internal/config/config_test.go @@ -77,7 +77,7 @@ func TestValidate_AgentKey(t *testing.T) { require.NoError(t, err) } else { require.Error(t, err) - require.Contains(t, err.Error(), "BACKUP_AGENT_KEY") + require.Contains(t, err.Error(), "BACKUPY_AGENT_KEY") } }) } diff --git a/apps/agent/internal/logging/logging.go b/apps/agent/internal/logging/logging.go index 8ba6d9d..35aa710 100644 --- a/apps/agent/internal/logging/logging.go +++ b/apps/agent/internal/logging/logging.go @@ -5,7 +5,7 @@ // are eventually streamed to the server, so structured form is mandatory). // The dev profile lowers verbosity by disabling source positions. // -// BACKUP_AGENT_KEY is never logged — see config.Config which tags it +// BACKUPY_AGENT_KEY is never logged — see config.Config which tags it // `json:"-"` and the redactKey helper here for defence-in-depth. package logging diff --git a/apps/agent/internal/state/crypto.go b/apps/agent/internal/state/crypto.go index dec77db..5cc7f7e 100644 --- a/apps/agent/internal/state/crypto.go +++ b/apps/agent/internal/state/crypto.go @@ -1,8 +1,8 @@ // State-at-rest encryption helpers. // // All bucket *values* are wrapped with AES-256-GCM using a key derived from -// BACKUP_AGENT_KEY via HKDF-SHA256 (per docs/03-agent-spec.md → -// "Шифрование state опционально (key derived из BACKUP_AGENT_KEY)"). +// BACKUPY_AGENT_KEY via HKDF-SHA256 (per docs/03-agent-spec.md → +// "Шифрование state опционально (key derived из BACKUPY_AGENT_KEY)"). // // Wire format on disk: // diff --git a/apps/agent/internal/state/state.go b/apps/agent/internal/state/state.go index 9ef5ce2..4afbf46 100644 --- a/apps/agent/internal/state/state.go +++ b/apps/agent/internal/state/state.go @@ -1,5 +1,5 @@ // Package state owns the agent's persistent on-disk state — a BoltDB file -// at $BACKUP_STATE_DIR/state.db. +// at $BACKUPY_STATE_DIR/state.db. // // Buckets: // @@ -8,7 +8,7 @@ // "registry" — session metadata: last session_id, server_time, heartbeat. // "logs_buffer" — rate-limited LogEvent buffer when server is unreachable. // -// All bucket values are encrypted with AES-256-GCM keyed by HKDF(BACKUP_AGENT_KEY). +// All bucket values are encrypted with AES-256-GCM keyed by HKDF(BACKUPY_AGENT_KEY). // See crypto.go for the wire format. // // Concurrency: bbolt serialises write transactions itself, so the Store is diff --git a/apps/agent/internal/wss/client.go b/apps/agent/internal/wss/client.go index 71ba351..fb599dc 100644 --- a/apps/agent/internal/wss/client.go +++ b/apps/agent/internal/wss/client.go @@ -70,7 +70,7 @@ type Config struct { Capabilities []string // AllowInsecure permits ws:// / http:// dial schemes when ServerURL // uses one. Production must leave this false — it matches the - // agent's BACKUP_DEV_ALLOW_INSECURE bootstrap flag. + // agent's BACKUPY_DEV_ALLOW_INSECURE bootstrap flag. AllowInsecure bool } diff --git a/docs/03-agent-spec.md b/docs/03-agent-spec.md index 32dddfe..b868230 100644 --- a/docs/03-agent-spec.md +++ b/docs/03-agent-spec.md @@ -11,8 +11,8 @@ services: backup-agent: image: backupservice/agent:latest environment: - BACKUP_SERVER_URL: https://backupy.ru - BACKUP_AGENT_KEY: ${BACKUP_KEY} + BACKUPY_SERVER_URL: https://backupy.ru + BACKUPY_AGENT_KEY: ${BACKUP_KEY} volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - backup-agent-state:/var/lib/backup-agent @@ -26,10 +26,10 @@ volumes: | Имя | Назначение | Required | |---|---|---| -| `BACKUP_SERVER_URL` | Адрес control plane | да | -| `BACKUP_AGENT_KEY` | Ключ агента (секрет) | да | -| `BACKUP_LOG_LEVEL` | trace/debug/info/warn/error, default info | нет | -| `BACKUP_STATE_DIR` | Путь к state, default `/var/lib/backup-agent` | нет | +| `BACKUPY_SERVER_URL` | Адрес control plane | да | +| `BACKUPY_AGENT_KEY` | Ключ агента (секрет) | да | +| `BACKUPY_LOG_LEVEL` | trace/debug/info/warn/error, default info | нет | +| `BACKUPY_STATE_DIR` | Путь к state, default `/var/lib/backup-agent` | нет | Всё остальное (targets, schedules, S3 creds, retention, hooks) — приходит с сервера через `ConfigUpdate`. @@ -46,7 +46,7 @@ volumes: ### Persistent state в volume - SQLite или BoltDB в `/var/lib/backup-agent/state.db`. - Хранит: текущий config, очередь jobs, локальные логи, последний known config_version. -- Шифрование state опционально (key derived из BACKUP_AGENT_KEY). +- Шифрование state опционально (key derived из BACKUPY_AGENT_KEY). ### WSS-канал - Один long-lived connection на agent_id. @@ -114,7 +114,7 @@ volumes: - TLS 1.3 ко всем endpoint'ам. - Pinning публичного ключа сервера (зашит в бинарь). - Docker socket монтируется read-only. -- `BACKUP_AGENT_KEY` никогда не пишется в логи. +- `BACKUPY_AGENT_KEY` никогда не пишется в логи. - Локальный state шифруется (опционально включается). - Healthcheck endpoint (если будет) — только на localhost. - Capabilities контейнера: drop ALL.