From f42a94ee7a34d01f5a3ac8bc05dd3c99ffc36e25 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 31 Aug 2026 13:26:50 +0300 Subject: [PATCH] fix(authority): use isolated service credential mount --- cmd/commit-runtime/control_plane_authority.go | 9 +++--- .../control_plane_authority_test.go | 29 +++++++++++++++++++ .../control_plane_authority_test.go | 14 +++++++++ cmd/core-runtime/main.go | 18 ++++++++++-- deploy/docker-compose.candidate-slot.yml | 3 +- 5 files changed, 66 insertions(+), 7 deletions(-) diff --git a/cmd/commit-runtime/control_plane_authority.go b/cmd/commit-runtime/control_plane_authority.go index 461db6f..dfd679f 100644 --- a/cmd/commit-runtime/control_plane_authority.go +++ b/cmd/commit-runtime/control_plane_authority.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "os" + "path/filepath" "strings" "time" @@ -22,7 +23,7 @@ import ( const ( delegatedControlPlaneActor identity.ActorID = "domovoy-control-plane-actor" delegatedControlPlanePrincipal identity.PrincipalID = "domovoy-control-plane-principal" - delegatedControlPlaneTokenPath = "/run/secrets/control_plane_service_token" + delegatedControlPlaneTokenFile = "control_plane_service_token" ) type delegatedControlPlaneAuthoritySource interface { @@ -157,13 +158,13 @@ func delegatedControlPlaneCredential() (string, error) { if ref == "" { return "", errors.New("control-plane service credential ref is required") } - root := os.Getenv("SECRET_BACKEND_ROOT") + root := os.Getenv("CONTROL_PLANE_SERVICE_SECRET_ROOT") if root == "" { - root = "/run/secrets" + root = "/run/control-plane-secrets" } resolver, err := platformsecrets.NewFileResolver(root) if err != nil { return "", err } - return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumer: secretcontracts.RuntimeConsumerCommit, Path: delegatedControlPlaneTokenPath}) + return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumer: secretcontracts.RuntimeConsumerCommit, Path: filepath.Join(root, delegatedControlPlaneTokenFile)}) } diff --git a/cmd/commit-runtime/control_plane_authority_test.go b/cmd/commit-runtime/control_plane_authority_test.go index 0c38840..445ebdd 100644 --- a/cmd/commit-runtime/control_plane_authority_test.go +++ b/cmd/commit-runtime/control_plane_authority_test.go @@ -5,6 +5,8 @@ import ( "errors" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" "time" @@ -20,6 +22,33 @@ type delegatedAuthorityFake struct { revoked bool } +func TestDelegatedAuthorityCredentialRotationRejectsOldToken(t *testing.T) { + root := t.TempDir() + tokenPath := filepath.Join(root, "control_plane_service_token") + if err := os.WriteFile(tokenPath, []byte("old-token\n"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF", "credential://domovoy/control-plane/service-token/v1") + t.Setenv("CONTROL_PLANE_SERVICE_SECRET_ROOT", root) + h := delegatedControlPlaneHandler{credential: delegatedControlPlaneCredential} + oldRequest := httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", nil) + oldRequest.Header.Set("Authorization", "Bearer old-token") + if !h.serviceAuthenticated(oldRequest) { + t.Fatal("active credential was rejected before rotation") + } + if err := os.WriteFile(tokenPath, []byte("new-token\n"), 0o600); err != nil { + t.Fatal(err) + } + if h.serviceAuthenticated(oldRequest) { + t.Fatal("old credential remained valid after rotation") + } + newRequest := httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", nil) + newRequest.Header.Set("Authorization", "Bearer new-token") + if !h.serviceAuthenticated(newRequest) { + t.Fatal("new credential was rejected after rotation") + } +} + func (f *delegatedAuthorityFake) LoadDelegatedControlPlaneGrant(context.Context, authority.GrantID) (authority.DelegatedControlPlaneGrant, error) { if f.grant.ID == "" { return authority.DelegatedControlPlaneGrant{}, errors.New("missing") diff --git a/cmd/core-runtime/control_plane_authority_test.go b/cmd/core-runtime/control_plane_authority_test.go index 16d8031..2ab6297 100644 --- a/cmd/core-runtime/control_plane_authority_test.go +++ b/cmd/core-runtime/control_plane_authority_test.go @@ -4,6 +4,8 @@ import ( "context" "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" "git.cin.su/m/Assistant/contracts/authority" @@ -23,3 +25,15 @@ func TestControlPlaneClientSendsOnlyServiceCredentialToAuthorityRuntime(t *testi t.Fatalf("authorize: %v", err) } } + +func TestControlPlaneCredentialUsesDedicatedSecretRoot(t *testing.T) { + root := t.TempDir() + if err := os.WriteFile(filepath.Join(root, controlPlaneServiceTokenFile), []byte("test-machine-token\n"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF", "credential://domovoy/control-plane/service-token/v1") + t.Setenv("CONTROL_PLANE_SERVICE_SECRET_ROOT", root) + if got, err := readControlPlaneServiceCredential(); err != nil || got != "test-machine-token" { + t.Fatalf("control-plane credential resolution failed: %v", err) + } +} diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index ccfde22..d7ded68 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -31,6 +31,7 @@ import ( "log" "net/http" "os" + "path/filepath" "regexp" "sort" "strconv" @@ -45,7 +46,7 @@ const ( epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password" interactionDBPasswordPath = "/run/secrets/interaction_db_password" selfDBPasswordPath = "/run/secrets/self_db_password" - controlPlaneServiceTokenPath = "/run/secrets/control_plane_service_token" + controlPlaneServiceTokenFile = "control_plane_service_token" ) const maxCreatorCredentialsBytes = 4 << 10 @@ -141,7 +142,7 @@ func main() { var delegatedAuthority *controlPlaneAuthority if os.Getenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF") != "" { delegatedAuthority = &controlPlaneAuthority{baseURL: envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"), client: &http.Client{Timeout: 5 * time.Second}, credential: func() (string, error) { - return readCredentialRefSecretPurpose("CONTROL_PLANE_SERVICE_TOKEN", "CONTROL_PLANE_SERVICE_CREDENTIAL_REF", controlPlaneServiceTokenPath, secretcontracts.SecretPurposeServiceAuthentication) + return readControlPlaneServiceCredential() }} } else { log.Printf("delegated control-plane authority is not configured; existing human control remains available") @@ -2088,6 +2089,19 @@ func readCredentialRefSecret(name, refName, fileName string) (string, error) { return readCredentialRefSecretPurpose(name, refName, fileName, secretcontracts.SecretPurposeDatabasePassword) } +func readControlPlaneServiceCredential() (string, error) { + ref := os.Getenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF") + if ref == "" { + return "", errors.New("CONTROL_PLANE_SERVICE_CREDENTIAL_REF is required") + } + root := envOr("CONTROL_PLANE_SERVICE_SECRET_ROOT", "/run/control-plane-secrets") + resolver, err := platformsecrets.NewFileResolver(root) + if err != nil { + return "", err + } + return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumer: secretcontracts.RuntimeConsumerCore, Path: filepath.Join(root, controlPlaneServiceTokenFile)}) +} + func readCredentialRefSecretPurpose(name, refName, fileName string, purpose secretcontracts.SecretPurpose) (string, error) { if _, directSet := os.LookupEnv(name); directSet { return "", fmt.Errorf("%s direct environment secret is forbidden; use the standard secret file", name) diff --git a/deploy/docker-compose.candidate-slot.yml b/deploy/docker-compose.candidate-slot.yml index d0b4790..f80cdae 100644 --- a/deploy/docker-compose.candidate-slot.yml +++ b/deploy/docker-compose.candidate-slot.yml @@ -23,7 +23,6 @@ services: SECRET_BACKEND_ROOT: /run/secrets volumes: - assistant_core_secrets:/run/secrets:ro - - assistant_control_plane_service_secrets:/run/control-plane-secrets:ro - assistant_research_sources:/var/lib/research-sources:ro networks: shared: @@ -55,6 +54,7 @@ services: CAPABILITY_POSTGRES_USER: capability_role CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD} CONTROL_PLANE_SERVICE_CREDENTIAL_REF: credential://domovoy/control-plane/service-token/v1 + CONTROL_PLANE_SERVICE_SECRET_ROOT: /run/control-plane-secrets SELF_POSTGRES_USER: self_role SELF_POSTGRES_CREDENTIAL_REF: credential://domovoy/self/db-password/v1 EPISTEMICS_POSTGRES_USER: epistemics_role @@ -71,6 +71,7 @@ services: RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087 volumes: - assistant_core_secrets:/run/secrets:ro + - assistant_control_plane_service_secrets:/run/control-plane-secrets:ro networks: shared: aliases: [core-runtime-candidate]