diff --git a/test/fakeintake/cmd/server/main.go b/test/fakeintake/cmd/server/main.go index 5ee4e114f7a9..07e1879d1d24 100644 --- a/test/fakeintake/cmd/server/main.go +++ b/test/fakeintake/cmd/server/main.go @@ -26,6 +26,7 @@ func main() { rcStatePath := flag.String("rc-state", "", "Remote Config: YAML file with initial config to preload") rcVersion := flag.Uint64("rc-version", 0, "Remote Config: initial version counter (default 1)") rcKeyPath := flag.String("rc-key-path", "", "Remote Config: ed25519 signing key path (default ~/.fakeintake/signing.key)") + rcKeyData := flag.String("rc-key-data", "", "Remote Config: hex-encoded 32-byte ed25519 seed (takes precedence over --rc-key-path; use for ephemeral envs)") flag.Parse() @@ -46,7 +47,9 @@ func main() { if *remoteConfig { fiOptions = append(fiOptions, fakeintake.WithRemoteConfig(*rcOrgUUID)) - if *rcKeyPath != "" { + if *rcKeyData != "" { + fiOptions = append(fiOptions, fakeintake.WithRemoteConfigKeyData(*rcKeyData)) + } else if *rcKeyPath != "" { fiOptions = append(fiOptions, fakeintake.WithRemoteConfigKeyPath(*rcKeyPath)) } if *rcVersion != 0 { diff --git a/test/fakeintake/server/rc.go b/test/fakeintake/server/rc.go index 16af179ab078..3a17e6608cd5 100644 --- a/test/fakeintake/server/rc.go +++ b/test/fakeintake/server/rc.go @@ -45,6 +45,7 @@ type rcServerState struct { rootJSON []byte keyPath string + keyData string // hex-encoded seed; takes precedence over keyPath when non-empty initialStatePath string } @@ -142,6 +143,20 @@ func WithRemoteConfigKeyPath(path string) Option { } } +// WithRemoteConfigKeyData supplies the ed25519 signing key as a hex-encoded +// 32-byte seed string. When set, the key is never written to disk and +// WithRemoteConfigKeyPath is ignored. Use this for ephemeral environments +// (e.g. ECS Fargate) where a fixed, pre-known key is required so the agent's +// config_root/director_root can be set at provisioning time. +func WithRemoteConfigKeyData(hexSeed string) Option { + return func(fi *Server) { + if fi.rc == nil { + return + } + fi.rc.keyData = hexSeed + } +} + // WithRemoteConfigVersion seeds the version counter (default 1). Used to keep // the agent's remote-config.db in sync across restarts. func WithRemoteConfigVersion(v uint64) Option { @@ -183,9 +198,25 @@ func (fi *Server) initRC() error { return nil } - priv, generated, err := rcstore.LoadOrCreateSigningKey(rc.keyPath) - if err != nil { - return fmt.Errorf("rc signing key: %w", err) + var ( + priv ed25519.PrivateKey + err error + ) + if rc.keyData != "" { + priv, err = rcstore.KeyFromHexSeed(rc.keyData) + if err != nil { + return fmt.Errorf("rc signing key (from --rc-key-data): %w", err) + } + log.Println("Remote Config: loaded signing key from --rc-key-data") + } else { + var generated bool + priv, generated, err = rcstore.LoadOrCreateSigningKey(rc.keyPath) + if err != nil { + return fmt.Errorf("rc signing key: %w", err) + } + if generated { + log.Println("Remote Config: generated new signing key — agent's remote-config.db must be flushed") + } } rc.signing = priv @@ -203,9 +234,6 @@ func (fi *Server) initRC() error { rc.rootJSON = root log.Printf("Remote Config: keyid=%s pubkey=%s", keyID, pubHex) - if generated { - log.Println("Remote Config: generated new signing key — agent's remote-config.db must be flushed") - } log.Printf("Remote Config: paste into datadog.yaml:\n remote_configuration.config_root: '%s'\n remote_configuration.director_root: '%s'", root, root) if rc.initialStatePath != "" { diff --git a/test/fakeintake/server/rcstore/keystore.go b/test/fakeintake/server/rcstore/keystore.go index 8dc54958fb2c..d997dac76b96 100644 --- a/test/fakeintake/server/rcstore/keystore.go +++ b/test/fakeintake/server/rcstore/keystore.go @@ -8,6 +8,7 @@ package rcstore import ( "crypto/ed25519" "crypto/rand" + "encoding/hex" "errors" "fmt" "os" @@ -24,6 +25,19 @@ func DefaultKeyPath() (string, error) { return filepath.Join(home, ".fakeintake", "signing.key"), nil } +// KeyFromHexSeed derives an ed25519 private key from a 64-character hex-encoded +// 32-byte seed. Returns an error when hexSeed is malformed. +func KeyFromHexSeed(hexSeed string) (ed25519.PrivateKey, error) { + seed, err := hex.DecodeString(hexSeed) + if err != nil { + return nil, fmt.Errorf("decode seed: %w", err) + } + if len(seed) != ed25519.SeedSize { + return nil, fmt.Errorf("expected %d-byte seed, got %d", ed25519.SeedSize, len(seed)) + } + return ed25519.NewKeyFromSeed(seed), nil +} + // LoadOrCreateSigningKey reads a 32-byte ed25519 seed from path. If path is // empty, falls back to DefaultKeyPath. If the file does not exist, a fresh // key is generated and written. The returned bool reports whether a new key