b3b89045f2
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>
47 lines
1.4 KiB
Go
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
|
|
}
|