Files
restic-manager/internal/store/host_credentials.go
T
steve 0ba56ed30d
CI / Test (linux/amd64) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Build (windows/amd64) (push) Has been cancelled
CI / Build (linux/amd64) (push) Has been cancelled
CI / Build (linux/arm64) (push) Has been cancelled
P1-32: server-side encrypted repo creds + push-on-hello
Operator-minted enrollment tokens now carry the repo URL/username/
password as one AEAD blob bound (via additional-data) to the token
hash. ConsumeEnrollmentToken re-encrypts under host_id and writes a
host_credentials row in the same tx as token-burn, so the binding
moves with the credential.

PUT /api/hosts/{id}/repo-credentials lets an operator edit creds
post-enrollment; merges with the existing blob, audits, and pushes
config.update if the agent is connected.

WS handler grows an OnHello hook that the HTTP layer wires to send
the host's decrypted creds as a config.update immediately after the
hello succeeds — synchronously, so a racing command.run lands after
the agent has its repo password.

Schema: 0002_host_credentials.sql adds enc_repo_creds to
enrollment_tokens and a host_credentials table (PK = host_id, FK
ON DELETE CASCADE).

Tests: round-trip token → consume → host_credentials with AAD swap
detection; no-creds path stays compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:38:35 +01:00

47 lines
1.4 KiB
Go

package store
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
)
// GetHostCredentials returns the AEAD-encrypted repo creds blob for
// the host, or ("", ErrNotFound) if no credential has ever been set.
// The caller decrypts using host_id as AEAD additional data.
func (s *Store) GetHostCredentials(ctx context.Context, hostID string) (string, error) {
row := s.db.QueryRowContext(ctx,
`SELECT enc_repo_creds FROM host_credentials WHERE host_id = ?`,
hostID)
var enc string
if err := row.Scan(&enc); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", ErrNotFound
}
return "", fmt.Errorf("store: get host credentials: %w", err)
}
return enc, nil
}
// SetHostCredentials replaces the host's encrypted repo creds blob.
// The caller has already encrypted using host_id as additional data.
func (s *Store) SetHostCredentials(ctx context.Context, hostID, encRepoCreds string) error {
if encRepoCreds == "" {
return fmt.Errorf("store: empty enc_repo_creds")
}
now := time.Now().UTC().Format(time.RFC3339Nano)
_, err := s.db.ExecContext(ctx,
`INSERT INTO host_credentials (host_id, enc_repo_creds, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(host_id) DO UPDATE SET
enc_repo_creds = excluded.enc_repo_creds,
updated_at = excluded.updated_at`,
hostID, encRepoCreds, now)
if err != nil {
return fmt.Errorf("store: set host credentials: %w", err)
}
return nil
}