merge: GEN2-SECRETS-001 into delegated authority
# Conflicts: # deploy/docker-compose.prod.yml # dev/ACTIVE_TASK.json # docs/CREDENTIALS_AND_SECRETS.md
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRoot = "/run/control-plane-secrets"
|
||||
defaultRuntimeUID = 100
|
||||
defaultRuntimeGID = 101
|
||||
)
|
||||
|
||||
func main() {
|
||||
reason := flag.String("reason", "", "rotation reason: owner-requested-rotation or exposure-detected")
|
||||
flag.Parse()
|
||||
if flag.NArg() != 1 {
|
||||
fail("usage: secret-control-plane [ensure|rotate]")
|
||||
}
|
||||
store, err := platformsecrets.NewLocalMachineCredentialStore(defaultRoot, defaultRuntimeUID, defaultRuntimeGID, nil)
|
||||
if err != nil {
|
||||
fail("control-plane credential store unavailable")
|
||||
}
|
||||
spec := platformsecrets.ControlPlaneServiceTokenSpec()
|
||||
switch strings.TrimSpace(flag.Arg(0)) {
|
||||
case "ensure":
|
||||
result, err := store.Ensure(spec)
|
||||
if err != nil {
|
||||
fail("control-plane credential provisioning failed")
|
||||
}
|
||||
if result.Created {
|
||||
fmt.Println("control-plane credential provisioned")
|
||||
return
|
||||
}
|
||||
fmt.Println("control-plane credential already active")
|
||||
case "rotate":
|
||||
if !validRotationReason(*reason) {
|
||||
fail("rotation reason is invalid")
|
||||
}
|
||||
if _, err := store.Rotate(spec, *reason); err != nil {
|
||||
fail("control-plane credential rotation failed")
|
||||
}
|
||||
fmt.Println("control-plane credential rotated")
|
||||
default:
|
||||
fail("unsupported control-plane credential operation")
|
||||
}
|
||||
}
|
||||
|
||||
func validRotationReason(reason string) bool {
|
||||
switch strings.TrimSpace(reason) {
|
||||
case "owner-requested-rotation", "exposure-detected":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func fail(message string) {
|
||||
fmt.Fprintln(os.Stderr, message)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSecretControlPlaneDefaultsUseDedicatedProtectedRoot(t *testing.T) {
|
||||
if defaultRoot != "/run/control-plane-secrets" || defaultRuntimeUID <= 0 || defaultRuntimeGID <= 0 {
|
||||
t.Fatal("unsafe control-plane defaults")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRotationReasonsAreBoundedAndNeverArbitraryDiagnosticText(t *testing.T) {
|
||||
if !validRotationReason("owner-requested-rotation") || !validRotationReason("exposure-detected") || validRotationReason("Bearer secret-value") {
|
||||
t.Fatal("unsafe rotation reason validation")
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ 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:
|
||||
@@ -139,6 +140,9 @@ services:
|
||||
volumes:
|
||||
assistant_core_secrets:
|
||||
external: true
|
||||
assistant_control_plane_service_secrets:
|
||||
name: assistant_control_plane_service_secrets
|
||||
external: true
|
||||
assistant_research_sources:
|
||||
name: assistant_research_sources
|
||||
external: true
|
||||
|
||||
@@ -71,6 +71,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
|
||||
@@ -86,6 +87,7 @@ services:
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
|
||||
interval: 10s
|
||||
@@ -241,6 +243,7 @@ services:
|
||||
AUTHORITY_POSTGRES_USER: authority_role
|
||||
AUTHORITY_POSTGRES_PASSWORD: ${AUTHORITY_DB_PASSWORD}
|
||||
CONTROL_PLANE_SERVICE_CREDENTIAL_REF: credential://domovoy/control-plane/service-token/v1
|
||||
CONTROL_PLANE_SERVICE_SECRET_ROOT: /run/control-plane-secrets
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
|
||||
@@ -251,8 +254,18 @@ services:
|
||||
depends_on: [core-runtime, continuity-runtime, notification-receiver]
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
|
||||
- assistant_notification_writer_secret:/run/notification-writer:ro
|
||||
|
||||
secret-control-plane:
|
||||
profiles: ["secret-control-plane"]
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: secret-control-plane, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
user: "0:0"
|
||||
restart: "no"
|
||||
command: ["ensure"]
|
||||
volumes:
|
||||
- assistant_control_plane_service_secrets:/run/control-plane-secrets
|
||||
|
||||
cognitive-worker:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: cognitive-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
@@ -367,6 +380,9 @@ volumes:
|
||||
assistant_nats:
|
||||
assistant_core_secrets:
|
||||
external: true
|
||||
assistant_control_plane_service_secrets:
|
||||
name: assistant_control_plane_service_secrets
|
||||
external: true
|
||||
assistant_notification_receipts:
|
||||
assistant_notification_writer_secret:
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# This is the only production writer for the dedicated control-plane machine
|
||||
# credential volume. It never receives, prints, or exports a secret value.
|
||||
BASE_DIR="${BASE_DIR:-/home/m/assistant-deploy}"
|
||||
REPO_DIR="${REPO_DIR:-$BASE_DIR/repo}"
|
||||
ENV_FILE="${ENV_FILE:-$BASE_DIR/production-compose.env}"
|
||||
COMPOSE_FILE="$REPO_DIR/deploy/docker-compose.prod.yml"
|
||||
VOLUME="assistant_control_plane_service_secrets"
|
||||
ACTION="${1:-ensure}"
|
||||
|
||||
case "$ACTION" in
|
||||
ensure|rotate) ;;
|
||||
*) echo "unsupported machine credential action" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
[[ -f "$ENV_FILE" ]] || { echo "production compose environment is missing" >&2; exit 1; }
|
||||
[[ -f "$COMPOSE_FILE" ]] || { echo "production compose file is missing" >&2; exit 1; }
|
||||
|
||||
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
|
||||
|
||||
# `docker volume create` is idempotent and does not inspect or expose content.
|
||||
docker volume create "$VOLUME" >/dev/null
|
||||
revision="$(git -C "$REPO_DIR" rev-parse HEAD)"
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose build --build-arg "SOURCE_REVISION=$revision" secret-control-plane >/dev/null
|
||||
|
||||
if [[ "$ACTION" == "rotate" ]]; then
|
||||
# Only bounded, non-secret rotation reasons can reach the audit trail.
|
||||
reason="${ROTATION_REASON:-owner-requested-rotation}"
|
||||
case "$reason" in
|
||||
owner-requested-rotation|exposure-detected) ;;
|
||||
*) echo "invalid rotation reason" >&2; exit 2 ;;
|
||||
esac
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane -reason "$reason" rotate
|
||||
else
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane ensure
|
||||
fi
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Regression: the wrapper accepts only an operation name and never places a
|
||||
# plaintext credential on a Docker/Compose command line.
|
||||
script="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/provision-machine-credential.sh"
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
mkdir -p "$tmp/bin" "$tmp/repo/deploy"
|
||||
cp "$script" "$tmp/repo/deploy/provision-machine-credential.sh"
|
||||
printf 'placeholder\n' > "$tmp/repo/deploy/docker-compose.prod.yml"
|
||||
printf 'placeholder\n' > "$tmp/env"
|
||||
cat > "$tmp/bin/docker" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >> "${CAPTURE:?}"
|
||||
SH
|
||||
cat > "$tmp/bin/git" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
printf '0123456789abcdef0123456789abcdef01234567\n'
|
||||
SH
|
||||
chmod +x "$tmp/bin/docker" "$tmp/bin/git" "$tmp/repo/deploy/provision-machine-credential.sh"
|
||||
capture="$tmp/capture"
|
||||
PATH="$tmp/bin:$PATH" CAPTURE="$capture" REPO_DIR="$tmp/repo" ENV_FILE="$tmp/env" "$tmp/repo/deploy/provision-machine-credential.sh" ensure >/dev/null
|
||||
grep -qx 'volume create assistant_control_plane_service_secrets' "$capture"
|
||||
grep -q -- '--profile secret-control-plane run --rm --no-deps secret-control-plane ensure' "$capture"
|
||||
PATH="$tmp/bin:$PATH" CAPTURE="$capture" REPO_DIR="$tmp/repo" ENV_FILE="$tmp/env" ROTATION_REASON=owner-requested-rotation "$tmp/repo/deploy/provision-machine-credential.sh" rotate >/dev/null
|
||||
grep -q -- '--profile secret-control-plane run --rm --no-deps secret-control-plane -reason owner-requested-rotation rotate' "$capture"
|
||||
if grep -Eqi '(token=|password=|secret=|authorization:)' "$capture"; then
|
||||
echo "provisioning wrapper passed credential material" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Safe diagnostics only: artifact existence/readability and mount topology.
|
||||
# This script never reads or prints credential content.
|
||||
TOKEN_PATH="/run/control-plane-secrets/control_plane_service_token"
|
||||
VOLUME="assistant_control_plane_service_secrets"
|
||||
authorized=(assistant-core-runtime-1 assistant-commit-runtime-1)
|
||||
unauthorized=(assistant-creator-web-1 assistant-cognitive-worker-1 assistant-research-runtime-1 assistant-continuity-runtime-1 assistant-agent-worker-1)
|
||||
|
||||
for container in "${authorized[@]}"; do
|
||||
docker inspect "$container" >/dev/null
|
||||
docker exec "$container" sh -c "test -r '$TOKEN_PATH'"
|
||||
docker inspect -f '{{range .Mounts}}{{if eq .Name "assistant_control_plane_service_secrets"}}{{.Destination}} {{.RW}}{{end}}{{end}}' "$container" | grep -qx '/run/control-plane-secrets false'
|
||||
done
|
||||
|
||||
for container in "${unauthorized[@]}"; do
|
||||
docker inspect "$container" >/dev/null
|
||||
if docker inspect -f '{{range .Mounts}}{{println .Name}}{{end}}' "$container" | grep -qx "$VOLUME"; then
|
||||
echo "unauthorized runtime has the control-plane credential mount" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "machine credential mount topology verified"
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"task_id": "GEN2-SECRETS-001",
|
||||
"task_revision": 2,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Provide an idempotent, local-only, cryptographically secure machine-credential provisioner and rotation mechanism on the existing protected Docker secret control plane, initially for the delegated control-plane service token, without creating a second semantic owner or exposing a secret value.",
|
||||
"requirement_ids": ["REQ-GEN2-SECRETS-001-LOCAL-MACHINE-CREDENTIALS", "REQ-GEN2-AUTHORITY-001-DELEGATED-CONTROL-PLANE"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["technical protected secret file backend", "production deployment/bootstrap secret provisioning", "targeted Core/Commit service-authentication mount wiring", "targeted tests and evidence"],
|
||||
"paths": ["platform/secrets/**", "contracts/secrets/types.go", "cmd/secret-control-plane/**", "cmd/core-runtime/main.go", "cmd/commit-runtime/control_plane_authority.go", "deploy/images/runtime.Dockerfile", "deploy/docker-compose.prod.yml", "deploy/docker-compose.candidate-slot.yml", "deploy/provision-machine-credential.sh", "deploy/provision-machine-credential_test.sh", "deploy/verify-machine-credential.sh", "docs/CREDENTIALS_AND_SECRETS.md", "docs/evidence/**", "development/GEN2_TASK_INDEX.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-SECRETS-001-local-machine-credential-provisioning.json"]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "cmd/devctl/**", "contracts/authority/**", "modules/authority/**", "adapters/postgres/**", "database/**", "modules/identity/**", "modules/capability/**", "cmd/cognitive-worker/**", "cmd/creator-web/**", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**", "go.mod", "go.sum"]
|
||||
},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing CredentialRef and FileResolver", "existing Docker protected-volume deployment mechanism"], "may_change": ["typed SecretPurpose/consumer metadata only if required for the provisioner"]},
|
||||
"semantic_owner": "No new semantic owner is introduced. platform/secrets remains a technical bootstrap/resolution mechanism; Identity, Authority, Capability and Configuration ownership remain unchanged.",
|
||||
"canonical_state": ["credential values live only in protected local artifacts, never in Configuration, Memory, context, PostgreSQL, Compose environment, Git, logs or evidence", "machine authentication is distinct from Authority grants", "metadata/audit is separate from the value and contains no plaintext or reversible material", "the control-plane token is mounted read-only only to Core and Commit runtimes"],
|
||||
"state_transitions": ["ABSENT -> ACTIVE version one via idempotent CSPRNG provisioning", "ACTIVE -> ROTATING -> ACTIVE new version after atomic artifact replacement and controlled verification", "ACTIVE -> COMPROMISED/REVOKED -> replacement version with audit retained"],
|
||||
"side_effects_and_privileges": ["a one-shot local control-plane container may create or rotate only its dedicated external Docker volume", "Core and Commit get read-only mounts; all other runtimes receive no mount", "no plaintext environment variable is added"],
|
||||
"persistence_and_migrations": ["no application database schema or canonical writer changes", "secret metadata and append-only audit reside separately from the secret value inside the protected control-plane volume", "rollback retains prior metadata and revokes only after a verified replacement"],
|
||||
"failure_retry_restart": ["restart is idempotent and preserves the active artifact", "partial writes are avoided by atomic replacement", "a missing or unreadable artifact fails service authentication closed", "rotation never emits a credential and leaves a valid active artifact on failure"],
|
||||
"assumptions": ["docker.cin.su permits creation of a dedicated external Docker volume by the existing production deployment control plane", "the runtime image can execute a one-shot local provisioner as the control-plane writer", "the existing protected secret volume pattern is the approved production mechanism"],
|
||||
"acceptance_scenarios": ["missing credential is generated once with crypto/rand and then reused across restart", "metadata and audit are separate from the value", "Core and Commit mounts are read-only and no other runtime mounts the dedicated token volume", "rotation accepts the new credential and rejects the old credential", "logs and diagnostics redact known secret headers/fields", "a valid service credential alone does not bypass the existing delegated Authority grant checks"],
|
||||
"required_tests": ["targeted go test ./platform/secrets ./cmd/secret-control-plane", "targeted go vet ./platform/secrets ./cmd/secret-control-plane", "compose config validation", "provisioner shell regression", "preflight", "check-diff", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
# DOMOVOY Gen2 — Task Index
|
||||
|
||||
## GEN2-SECRETS-001 — Local Machine Credential Provisioning & Rotation
|
||||
|
||||
Bounded post-M5 remediation. Provides the technical protected-secret control-plane mechanism required by `GEN2-AUTHORITY-001`; it does not change semantic ownership or grant Authority.
|
||||
|
||||
Это исполняемый индекс task packets. `ARCH-GEN2-000` соответствует разделу `GEN2-000` Master Plan: префикс `ARCH-` нужен текущему `devctl`, чтобы последующие packets могли ссылаться на него через `architecture_revision`.
|
||||
|
||||
| Порядок | Task | Milestone | Gate | Base ref | Зависимость | Файл |
|
||||
|
||||
@@ -23,7 +23,9 @@ Each privileged runtime gets only its required secret/capability bindings. `comm
|
||||
|
||||
The Core SELF learning repository uses the existing `assistant_core_secrets` volume and credential-reference mechanism with `self_role` and `credential://domovoy/self/db-password/v1`. The secret is consumed from the fixed `/run/secrets/self_db_password` path; its value is never placed in Compose environment, Git, logs or ordinary configuration. The role and schema already exist; this wiring does not create a role or broaden privileges.
|
||||
|
||||
Delegated non-interactive control-plane work is separately authenticated. The external `assistant_core_secrets` control plane must materialize `control_plane_service_token`, mode `0400`, for `core-runtime` and the existing Authority/Commit runtime only. Compose carries only `credential://domovoy/control-plane/service-token/v1`; trusted runtimes resolve the fixed file below `/run/secrets`. The service token is not a Creator session, cannot impersonate a human, and must be rotated or withdrawn in the secret backend. Removing the token disables new delegated operations; revoking the durable Authority grant also blocks them without deleting audit history.
|
||||
Delegated non-interactive control-plane work is separately authenticated. The local `secret-control-plane` one-shot control-plane service provisions `control_plane_service_token` with `crypto/rand` into the dedicated external `assistant_control_plane_service_secrets` volume. The token artifact is mode `0400`; metadata and audit are separate protected files and contain no value or reversible fingerprint. Only `core-runtime` and the existing Authority/Commit runtime mount that volume read-only at `/run/control-plane-secrets`; no model, Skill, SELF, creator-web, cognitive-worker or research-runtime receives the mount. Compose carries only the non-secret credential reference `credential://domovoy/control-plane/service-token/v1`.
|
||||
|
||||
The provisioner is idempotent: restart reuses an existing safe active artifact. Rotation creates a new value by atomic replacement, records non-secret metadata/audit, and never accepts a plaintext environment variable. The service token is not a Creator session, cannot impersonate a human, and never grants Authority by itself. Removing or rotating the token disables/refreshes service authentication; revoking the durable Authority grant also blocks new delegated operations without deleting audit history.
|
||||
|
||||
Creator authentication secrets are stored in the external Docker volume `assistant_core_secrets` and mounted read-only only at `/run/secrets` in `core-runtime`. The production container environment contains `_FILE` paths, not the secret values. Files are owned by runtime uid 100/gid 101 with mode 0400. `CREATOR_USERNAME` is not secret and remains an environment value. Development may continue to use direct environment values, but a direct value and its matching `_FILE` setting must never be configured together.
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# GEN2-SECRETS-001 — local machine credential provisioning
|
||||
|
||||
Date: 2026-08-31
|
||||
|
||||
## Design evidence
|
||||
|
||||
`platform/secrets` is the existing technical protected-file mechanism. This
|
||||
task extends it without adding a semantic Secrets owner, a database table, or
|
||||
a Configuration/Memory writer.
|
||||
|
||||
The dedicated Docker volume is named
|
||||
`assistant_control_plane_service_secrets`. Its sole writer is the one-shot
|
||||
`secret-control-plane` control-plane container. Its only consumers are
|
||||
`core-runtime` and `commit-runtime`, mounted read-only at
|
||||
`/run/control-plane-secrets`. The metadata directory is distinct from the
|
||||
token artifact and records purpose, owner, version, consumers, status,
|
||||
generation method, and non-secret audit actions only.
|
||||
|
||||
The provisioner uses `crypto/rand`, writes via an atomic file replacement,
|
||||
and has no plaintext environment input. It reports only safe operation status.
|
||||
The data policy marks the artifact as not indexable, embeddable, retrievable,
|
||||
or model-visible.
|
||||
|
||||
## Local FAST evidence
|
||||
|
||||
The following checks completed successfully before production execution:
|
||||
|
||||
- targeted `go test ./platform/secrets ./cmd/secret-control-plane`;
|
||||
- targeted `go vet ./platform/secrets ./cmd/secret-control-plane`;
|
||||
- POSIX shell regression for the provisioning wrapper, executed under Git
|
||||
Bash because the Windows WSL shim cannot translate the mapped workspace;
|
||||
- static Docker Compose validation for production and candidate files.
|
||||
|
||||
No credential value, fingerprint, length, header, or plaintext environment
|
||||
value was read, printed, copied, committed, or included in this evidence.
|
||||
|
||||
## Production control-plane evidence
|
||||
|
||||
The one-shot provisioner was run from a detached `gen2-integration` worktree
|
||||
on `docker.cin.su`. It created the dedicated external volume and provisioned
|
||||
the credential once. A repeat `ensure` reported that the already active
|
||||
artifact was reused. A bounded owner-requested rotation then completed.
|
||||
|
||||
For creation, idempotent ensure, and rotation, the stable Core, Commit,
|
||||
Creator Web, Cognitive Worker, and Research Runtime container identities were
|
||||
recorded before and after the one-shot operation and were unchanged. The
|
||||
verification checked only artifact and separate metadata/audit file presence;
|
||||
it did not read their content.
|
||||
|
||||
At this point the credential is provisioned but not yet used by the stable
|
||||
runtime. Consumer mount and service-authentication acceptance are completed
|
||||
only when `GEN2-AUTHORITY-001` is merged and deployed as its own controlled
|
||||
candidate ReleaseSet.
|
||||
@@ -0,0 +1,399 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
)
|
||||
|
||||
const (
|
||||
ControlPlaneServiceTokenID = "control_plane_service_token"
|
||||
MachineCredentialOwner = "platform.secrets.local-control-plane"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnsupportedMachineCredential = errors.New("unsupported machine credential")
|
||||
ErrCredentialArtifactUnsafe = errors.New("machine credential artifact is unsafe")
|
||||
)
|
||||
|
||||
// MachineCredentialKind describes technical credential material. It conveys no
|
||||
// authority and no secret value.
|
||||
type MachineCredentialKind string
|
||||
|
||||
const (
|
||||
MachineCredentialServiceToken MachineCredentialKind = "SERVICE_TO_SERVICE_TOKEN"
|
||||
MachineCredentialDatabaseRole MachineCredentialKind = "DATABASE_ROLE_PASSWORD"
|
||||
MachineCredentialInternalAPI MachineCredentialKind = "INTERNAL_API_CREDENTIAL"
|
||||
)
|
||||
|
||||
type MachineCredentialStatus string
|
||||
|
||||
const (
|
||||
MachineCredentialActive MachineCredentialStatus = "ACTIVE"
|
||||
MachineCredentialRotating MachineCredentialStatus = "ROTATING"
|
||||
MachineCredentialCompromised MachineCredentialStatus = "COMPROMISED"
|
||||
MachineCredentialRevoked MachineCredentialStatus = "REVOKED"
|
||||
)
|
||||
|
||||
type MachineCredentialSpec struct {
|
||||
ID string
|
||||
Kind MachineCredentialKind
|
||||
Purpose secretcontracts.SecretPurpose
|
||||
Consumers []secretcontracts.RuntimeConsumer
|
||||
Bytes int
|
||||
}
|
||||
|
||||
type MachineCredentialMetadata struct {
|
||||
SecretID string `json:"secret_id"`
|
||||
Kind MachineCredentialKind `json:"kind"`
|
||||
Purpose secretcontracts.SecretPurpose `json:"purpose"`
|
||||
Owner string `json:"owner"`
|
||||
Version int `json:"version"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
RotatedAt time.Time `json:"rotated_at,omitempty"`
|
||||
Consumers []secretcontracts.RuntimeConsumer `json:"consumers"`
|
||||
Status MachineCredentialStatus `json:"status"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
GenerationMethod string `json:"generation_method"`
|
||||
DataPolicy MachineCredentialDataPolicy `json:"data_policy"`
|
||||
}
|
||||
|
||||
// MachineCredentialDataPolicy is explicit technical handling policy. It is
|
||||
// metadata only; it never contains or resolves a credential value.
|
||||
type MachineCredentialDataPolicy struct {
|
||||
Indexable bool `json:"indexable"`
|
||||
Embeddable bool `json:"embeddable"`
|
||||
Retrievable bool `json:"retrievable"`
|
||||
ModelVisible bool `json:"model_visible"`
|
||||
}
|
||||
|
||||
type MachineCredentialAudit struct {
|
||||
SecretID string `json:"secret_id"`
|
||||
Version int `json:"version"`
|
||||
Action string `json:"action"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
type MachineCredentialResult struct {
|
||||
Metadata MachineCredentialMetadata
|
||||
Created bool
|
||||
Rotated bool
|
||||
}
|
||||
|
||||
// LocalMachineCredentialStore is the one writer for a dedicated protected
|
||||
// volume. The value and metadata always have separate files.
|
||||
type LocalMachineCredentialStore struct {
|
||||
root string
|
||||
metadata string
|
||||
runtimeUID int
|
||||
runtimeGID int
|
||||
now func() time.Time
|
||||
random io.Reader
|
||||
}
|
||||
|
||||
func NewLocalMachineCredentialStore(root string, runtimeUID, runtimeGID int, now func() time.Time) (*LocalMachineCredentialStore, error) {
|
||||
root = strings.TrimSpace(root)
|
||||
if root == "" {
|
||||
return nil, fmt.Errorf("machine credential root: %w", ErrPathOutsideRoot)
|
||||
}
|
||||
clean, err := filepath.Abs(filepath.Clean(root))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("machine credential root: %w", err)
|
||||
}
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
return &LocalMachineCredentialStore{root: clean, metadata: filepath.Join(clean, ".metadata"), runtimeUID: runtimeUID, runtimeGID: runtimeGID, now: now, random: rand.Reader}, nil
|
||||
}
|
||||
|
||||
func ControlPlaneServiceTokenSpec() MachineCredentialSpec {
|
||||
return MachineCredentialSpec{
|
||||
ID: ControlPlaneServiceTokenID,
|
||||
Kind: MachineCredentialServiceToken,
|
||||
Purpose: secretcontracts.SecretPurposeServiceAuthentication,
|
||||
Consumers: []secretcontracts.RuntimeConsumer{secretcontracts.RuntimeConsumerCore, secretcontracts.RuntimeConsumerCommit},
|
||||
Bytes: 48,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) Ensure(spec MachineCredentialSpec) (MachineCredentialResult, error) {
|
||||
if err := validateMachineCredentialSpec(spec); err != nil {
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
if err := s.prepareDirectories(); err != nil {
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
if info, err := os.Lstat(s.artifactPath(spec)); err == nil {
|
||||
if !safeArtifact(info) {
|
||||
return MachineCredentialResult{}, ErrCredentialArtifactUnsafe
|
||||
}
|
||||
metadata, err := s.ensureMetadata(spec, false, "")
|
||||
return MachineCredentialResult{Metadata: metadata}, err
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
metadata, err := s.createVersion(spec, 1, "CREATED", "")
|
||||
if err != nil {
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
return MachineCredentialResult{Metadata: metadata, Created: true}, nil
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) Rotate(spec MachineCredentialSpec, reason string) (MachineCredentialResult, error) {
|
||||
if err := validateMachineCredentialSpec(spec); err != nil {
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
if err := s.prepareDirectories(); err != nil {
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
current, err := s.readMetadata(spec)
|
||||
if err != nil {
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
if current.Status != MachineCredentialActive {
|
||||
return MachineCredentialResult{}, fmt.Errorf("machine credential status %s cannot rotate", current.Status)
|
||||
}
|
||||
rotating := current
|
||||
rotating.Status = MachineCredentialRotating
|
||||
if err := s.writeMetadata(spec, rotating); err != nil {
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
if err := s.appendAudit(MachineCredentialAudit{SecretID: spec.ID, Version: current.Version, Action: "ROTATION_STARTED", Reason: strings.TrimSpace(reason), At: s.now().UTC()}); err != nil {
|
||||
_ = s.writeMetadata(spec, current)
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
metadata, err := s.createVersion(spec, current.Version+1, "ROTATED", strings.TrimSpace(reason))
|
||||
if err != nil {
|
||||
_ = s.writeMetadata(spec, current)
|
||||
_ = s.appendAudit(MachineCredentialAudit{SecretID: spec.ID, Version: current.Version, Action: "ROTATION_FAILED", At: s.now().UTC()})
|
||||
return MachineCredentialResult{}, err
|
||||
}
|
||||
return MachineCredentialResult{Metadata: metadata, Rotated: true}, nil
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) createVersion(spec MachineCredentialSpec, version int, action, reason string) (MachineCredentialMetadata, error) {
|
||||
value, err := randomCredential(s.random, spec.Bytes)
|
||||
if err != nil {
|
||||
return MachineCredentialMetadata{}, err
|
||||
}
|
||||
if err := s.atomicArtifact(spec, value); err != nil {
|
||||
return MachineCredentialMetadata{}, err
|
||||
}
|
||||
now := s.now().UTC()
|
||||
metadata := MachineCredentialMetadata{SecretID: spec.ID, Kind: spec.Kind, Purpose: spec.Purpose, Owner: MachineCredentialOwner, Version: version, CreatedAt: now, Consumers: append([]secretcontracts.RuntimeConsumer(nil), spec.Consumers...), Status: MachineCredentialActive, Algorithm: "CSPRNG", GenerationMethod: "crypto/rand + base64url", DataPolicy: MachineCredentialDataPolicy{Indexable: false, Embeddable: false, Retrievable: false, ModelVisible: false}}
|
||||
if version > 1 {
|
||||
metadata.RotatedAt = now
|
||||
}
|
||||
if err := s.writeMetadata(spec, metadata); err != nil {
|
||||
return MachineCredentialMetadata{}, err
|
||||
}
|
||||
if err := s.appendAudit(MachineCredentialAudit{SecretID: spec.ID, Version: version, Action: action, Reason: reason, At: now}); err != nil {
|
||||
return MachineCredentialMetadata{}, err
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) ensureMetadata(spec MachineCredentialSpec, rotated bool, reason string) (MachineCredentialMetadata, error) {
|
||||
metadata, err := s.readMetadata(spec)
|
||||
if err == nil {
|
||||
return metadata, nil
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return MachineCredentialMetadata{}, err
|
||||
}
|
||||
now := s.now().UTC()
|
||||
metadata = MachineCredentialMetadata{SecretID: spec.ID, Kind: spec.Kind, Purpose: spec.Purpose, Owner: MachineCredentialOwner, Version: 1, CreatedAt: now, Consumers: append([]secretcontracts.RuntimeConsumer(nil), spec.Consumers...), Status: MachineCredentialActive, Algorithm: "CSPRNG", GenerationMethod: "preexisting protected artifact", DataPolicy: MachineCredentialDataPolicy{Indexable: false, Embeddable: false, Retrievable: false, ModelVisible: false}}
|
||||
if rotated {
|
||||
metadata.RotatedAt = now
|
||||
}
|
||||
if err := s.writeMetadata(spec, metadata); err != nil {
|
||||
return MachineCredentialMetadata{}, err
|
||||
}
|
||||
if err := s.appendAudit(MachineCredentialAudit{SecretID: spec.ID, Version: metadata.Version, Action: "ADOPTED", Reason: reason, At: now}); err != nil {
|
||||
return MachineCredentialMetadata{}, err
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) prepareDirectories() error {
|
||||
if err := os.MkdirAll(s.root, 0o710); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(s.root, 0o710); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.runtimeGID >= 0 && os.Geteuid() == 0 {
|
||||
if err := os.Chown(s.root, 0, s.runtimeGID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(s.metadata, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(s.metadata, 0o700)
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) atomicArtifact(spec MachineCredentialSpec, value string) error {
|
||||
temp, err := os.CreateTemp(s.root, ".credential-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempPath := temp.Name()
|
||||
defer os.Remove(tempPath)
|
||||
if err := temp.Chmod(0o400); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if s.runtimeUID >= 0 && s.runtimeGID >= 0 && os.Geteuid() == 0 {
|
||||
if err := temp.Chown(s.runtimeUID, s.runtimeGID); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := temp.WriteString(value + "\n"); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if err := temp.Sync(); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if err := temp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
target := s.artifactPath(spec)
|
||||
// Windows cannot replace a read-only file. On Linux the owning runtime can
|
||||
// still read this temporary mode; the final replacement remains atomic.
|
||||
if _, err := os.Lstat(target); err == nil {
|
||||
if err := os.Chmod(target, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempPath, target)
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) readMetadata(spec MachineCredentialSpec) (MachineCredentialMetadata, error) {
|
||||
contents, err := os.ReadFile(s.metadataPath(spec))
|
||||
if err != nil {
|
||||
return MachineCredentialMetadata{}, err
|
||||
}
|
||||
var metadata MachineCredentialMetadata
|
||||
if err := json.Unmarshal(contents, &metadata); err != nil {
|
||||
return MachineCredentialMetadata{}, err
|
||||
}
|
||||
if metadata.SecretID != spec.ID || metadata.Version < 1 || metadata.Purpose != spec.Purpose || metadata.Owner != MachineCredentialOwner {
|
||||
return MachineCredentialMetadata{}, ErrCredentialArtifactUnsafe
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) writeMetadata(spec MachineCredentialSpec, metadata MachineCredentialMetadata) error {
|
||||
contents, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return atomicPrivateFile(s.metadataPath(spec), contents)
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) appendAudit(audit MachineCredentialAudit) error {
|
||||
contents, err := json.Marshal(audit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.OpenFile(filepath.Join(s.metadata, audit.SecretID+".audit.jsonl"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := file.Write(append(contents, '\n')); err != nil {
|
||||
return err
|
||||
}
|
||||
return file.Sync()
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) artifactPath(spec MachineCredentialSpec) string {
|
||||
return filepath.Join(s.root, spec.ID)
|
||||
}
|
||||
func (s *LocalMachineCredentialStore) metadataPath(spec MachineCredentialSpec) string {
|
||||
return filepath.Join(s.metadata, spec.ID+".json")
|
||||
}
|
||||
|
||||
func randomCredential(source io.Reader, bytes int) (string, error) {
|
||||
if bytes < 32 {
|
||||
return "", fmt.Errorf("machine credential entropy must be at least 32 bytes")
|
||||
}
|
||||
value := make([]byte, bytes)
|
||||
if _, err := io.ReadFull(source, value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(value), nil
|
||||
}
|
||||
|
||||
func validateMachineCredentialSpec(spec MachineCredentialSpec) error {
|
||||
if strings.TrimSpace(spec.ID) == "" || strings.Contains(spec.ID, "/") || spec.Bytes < 32 || len(spec.Consumers) == 0 {
|
||||
return ErrUnsupportedMachineCredential
|
||||
}
|
||||
switch spec.Kind {
|
||||
case MachineCredentialServiceToken:
|
||||
if spec.Purpose != secretcontracts.SecretPurposeServiceAuthentication {
|
||||
return ErrUnsupportedMachineCredential
|
||||
}
|
||||
case MachineCredentialDatabaseRole:
|
||||
if spec.Purpose != secretcontracts.SecretPurposeDatabasePassword {
|
||||
return ErrUnsupportedMachineCredential
|
||||
}
|
||||
case MachineCredentialInternalAPI:
|
||||
if spec.Purpose != secretcontracts.SecretPurposeServiceAuthentication && spec.Purpose != secretcontracts.SecretPurposeProviderAPIKey {
|
||||
return ErrUnsupportedMachineCredential
|
||||
}
|
||||
default:
|
||||
return ErrUnsupportedMachineCredential
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func safeArtifact(info os.FileInfo) bool {
|
||||
if !info.Mode().IsRegular() || info.Size() <= 0 {
|
||||
return false
|
||||
}
|
||||
// Windows does not preserve POSIX permissions on a temporary test volume.
|
||||
return runtime.GOOS == "windows" || info.Mode().Perm() == 0o400
|
||||
}
|
||||
|
||||
func atomicPrivateFile(path string, contents []byte) error {
|
||||
temp, err := os.CreateTemp(filepath.Dir(path), ".metadata-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tempPath := temp.Name()
|
||||
defer os.Remove(tempPath)
|
||||
if err := temp.Chmod(0o600); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := temp.Write(contents); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if err := temp.Sync(); err != nil {
|
||||
temp.Close()
|
||||
return err
|
||||
}
|
||||
if err := temp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempPath, path)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
)
|
||||
|
||||
func TestMachineCredentialEnsureIsIdempotentAndKeepsValueOutOfMetadata(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
store, err := NewLocalMachineCredentialStore(t.TempDir(), -1, -1, func() time.Time { return now })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, err := store.Ensure(ControlPlaneServiceTokenSpec())
|
||||
if err != nil || !first.Created || first.Metadata.Version != 1 {
|
||||
t.Fatalf("first provision = %#v, %v", first, err)
|
||||
}
|
||||
second, err := store.Ensure(ControlPlaneServiceTokenSpec())
|
||||
if err != nil || second.Created || second.Metadata.Version != 1 {
|
||||
t.Fatalf("second provision = %#v, %v", second, err)
|
||||
}
|
||||
metadata, err := os.ReadFile(filepath.Join(store.metadata, ControlPlaneServiceTokenID+".json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
artifact, err := os.ReadFile(filepath.Join(store.root, ControlPlaneServiceTokenID))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(metadata), strings.TrimSpace(string(artifact))) {
|
||||
t.Fatal("metadata unexpectedly contains a credential value")
|
||||
}
|
||||
info, err := os.Stat(filepath.Join(store.root, ControlPlaneServiceTokenID))
|
||||
if err != nil || (runtime.GOOS != "windows" && info.Mode().Perm() != 0o400) {
|
||||
t.Fatalf("artifact mode = %v, %v", info.Mode(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachineCredentialSpecSupportsMachineCredentialKinds(t *testing.T) {
|
||||
cases := []MachineCredentialSpec{
|
||||
ControlPlaneServiceTokenSpec(),
|
||||
{ID: "database_role_password", Kind: MachineCredentialDatabaseRole, Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumers: []secretcontracts.RuntimeConsumer{secretcontracts.RuntimeConsumerCore}, Bytes: 32},
|
||||
{ID: "internal_api_credential", Kind: MachineCredentialInternalAPI, Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumers: []secretcontracts.RuntimeConsumer{secretcontracts.RuntimeConsumerCommit}, Bytes: 32},
|
||||
}
|
||||
for _, value := range cases {
|
||||
if err := validateMachineCredentialSpec(value); err != nil {
|
||||
t.Fatalf("credential kind %s is unsupported: %v", value.Kind, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachineCredentialRotateAtomicallyReplacesArtifactAndAdvancesVersion(t *testing.T) {
|
||||
store, err := NewLocalMachineCredentialStore(t.TempDir(), -1, -1, time.Now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.Ensure(ControlPlaneServiceTokenSpec()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(store.root, ControlPlaneServiceTokenID)
|
||||
oldValue, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rotated, err := store.Rotate(ControlPlaneServiceTokenSpec(), "exposure detected")
|
||||
if err != nil || !rotated.Rotated || rotated.Metadata.Version != 2 {
|
||||
t.Fatalf("rotation = %#v, %v", rotated, err)
|
||||
}
|
||||
newValue, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(oldValue) == string(newValue) {
|
||||
t.Fatal("rotation retained old credential")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachineCredentialRejectsUnsafeExistingArtifact(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.Mkdir(filepath.Join(root, ControlPlaneServiceTokenID), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store, err := NewLocalMachineCredentialStore(root, -1, -1, time.Now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.Ensure(ControlPlaneServiceTokenSpec()); err != ErrCredentialArtifactUnsafe {
|
||||
t.Fatalf("unsafe artifact error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package secrets
|
||||
|
||||
import "strings"
|
||||
|
||||
const Redacted = "REDACTED"
|
||||
|
||||
// RedactDiagnosticFields removes known credential/header values before a
|
||||
// diagnostics boundary. Callers must still avoid passing raw values at all.
|
||||
func RedactDiagnosticFields(fields map[string]string) map[string]string {
|
||||
redacted := make(map[string]string, len(fields))
|
||||
for key, value := range fields {
|
||||
if isSensitiveDiagnosticField(key) {
|
||||
redacted[key] = Redacted
|
||||
continue
|
||||
}
|
||||
redacted[key] = value
|
||||
}
|
||||
return redacted
|
||||
}
|
||||
|
||||
func isSensitiveDiagnosticField(key string) bool {
|
||||
key = strings.ToLower(strings.TrimSpace(key))
|
||||
return key == "authorization" || key == "proxy-authorization" || strings.Contains(key, "secret") || strings.Contains(key, "token") || strings.Contains(key, "password") || strings.Contains(key, "credential")
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package secrets
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRedactDiagnosticFieldsNeverReturnsKnownCredentialFields(t *testing.T) {
|
||||
fields := RedactDiagnosticFields(map[string]string{"Authorization": "Bearer value", "control_plane_service_token": "value", "status": "PASS"})
|
||||
if fields["Authorization"] != Redacted || fields["control_plane_service_token"] != Redacted || fields["status"] != "PASS" {
|
||||
t.Fatalf("unexpected diagnostic redaction: %#v", fields)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user