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" AcceptanceHarnessServiceTokenID = "acceptance_harness_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 AcceptanceHarnessServiceTokenSpec() MachineCredentialSpec { return MachineCredentialSpec{ID: AcceptanceHarnessServiceTokenID, Kind: MachineCredentialServiceToken, Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumers: []secretcontracts.RuntimeConsumer{secretcontracts.RuntimeConsumerCore}, 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) }