-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Support writing secrets to AWS secret manager. Use case is for a management cluster in a management account to write secrets to AWS SM in a non-prod account developers have access to. And a prod account developers do not have access to. Leverage high quality open source CNCF projects like the external-secrets-operator. Minimize code in holos-console to minimize support and upkeep of this service.
The initial idea from the Platform Engineering team is to use a PushSecret and have the holos-console store a reference to the PushSecret, then update an annotation on the PushSecret when a Secret is updated, triggering the sync to AWS SM. Evaluate the pros and cons of this approach.
Assume the user has Pod Identity configured and working well.
Design
Overview
Integrate holos-console with the External Secrets Operator (ESO) PushSecret CRD to sync K8s Secrets to AWS Secrets Manager. holos-console acts as a thin coordination layer: it stores references to PushSecret resources, bumps a force-sync annotation when secrets change, and reads PushSecret status conditions to surface sync state in the UI.
Architecture
flowchart LR
HC[holos-console<br/>Go server]
KS[K8s Secret]
PS[PushSecret<br/>ESO CRD]
ESO[ESO Controller]
AWS[AWS Secrets<br/>Manager]
HC -- "K8s Secret CRUD" --> KS
HC -- "Annotation bump<br/>force-sync timestamp" --> PS
HC -. "Read status<br/>conditions" .-> PS
KS -- "spec.selector<br/>references" --- PS
ESO -- "Watches" --> PS
ESO -- "Reads data" --> KS
ESO -- "PutSecretValue" --> AWS
How It Works
- Platform engineer pre-creates
SecretStore(orClusterSecretStore) andPushSecretresources via GitOps. The PushSecret'sspec.selector.secret.namereferences a K8s Secret managed by holos-console. - Platform engineer annotates the holos-console K8s Secret with
console.holos.run/push-secrets: '["my-push-secret"]'to register the association. This can also be done via the UI (Phase 4). - User creates or updates a secret via holos-console.
- holos-console reads the
push-secretsannotation, finds the referenced PushSecret resources, and patches each withconsole.holos.run/force-sync: "<unix-timestamp>". - ESO PushSecret controller detects the resource version change on the PushSecret (it watches PushSecret resources), triggers immediate reconciliation, reads the source K8s Secret, and pushes it to AWS SM.
- User sees push status in the UI (synced, pending, error) by reading PushSecret status conditions.
Storage Model
On the K8s Secret (managed by holos-console):
console.holos.run/push-secrets: '["my-push-secret","another-push-secret"]'
JSON array of PushSecret resource names in the same namespace.
On the PushSecret (managed by ESO, annotation added by holos-console):
console.holos.run/force-sync: "1706900000"
Unix timestamp set by holos-console to trigger reconciliation.
Approach Evaluation
Proposed approach: PushSecret annotation bump
Pros:
- Minimal code in holos-console — ~200-300 lines of Go (annotation read, dynamic client patch, status read). No AWS SDK dependency.
- Leverages ESO, a mature CNCF project, for all AWS API interactions, retries, error handling, and credential management.
- Pod Identity works seamlessly — ESO uses the service account's IAM role via Pod Identity. No credentials stored in holos-console.
- Multi-account support — Multiple PushSecrets can reference the same source secret, each targeting a different SecretStore (non-prod account, prod account). Platform engineers control topology via GitOps.
- Immediate sync — The PushSecret controller watches for resource version changes. Patching an annotation changes the resource version, triggering near-immediate reconciliation (~seconds).
- Consistent with existing patterns — holos-console already uses annotations for all metadata (grants, description, URL). Adding a push-secrets annotation is natural.
- Clean separation of concerns — holos-console manages secrets + RBAC, ESO handles cloud provider sync. Each component does what it's best at.
Cons:
- Requires ESO installed and configured — Acceptable prerequisite for the use case. ESO is already a standard component in CNCF-aligned platform stacks.
- Sync errors surface indirectly — PushSecret sync failures appear in ESO status conditions, not natively in holos-console. The
GetPushStatusRPC bridges this gap but adds a secondary read path. - Annotation references can drift — If a PushSecret is renamed or deleted outside holos-console, the annotation becomes stale. Mitigated by: (a) status check will show errors or "not found" for stale references, (b) UI can surface stale references for cleanup.
- Small sync latency — Annotation bump → controller watch event → reconcile → AWS API call. Typically completes within seconds, but not instantaneous.
Alternatives considered and rejected:
- Direct AWS SDK integration — Violates the "minimize code" goal. Would require maintaining AWS SDK dependency, multi-account assume-role logic, retry handling, and credential management — all of which ESO already handles.
- Custom Kubernetes controller — More code to maintain than the annotation-bump approach. ESO's PushSecret controller already implements the reconciliation loop with proper error handling and status reporting.
- ExternalSecret (pull direction) — Wrong direction. ExternalSecret pulls from external stores into K8s. We need push from K8s to AWS SM.
- holos-console creates PushSecrets — Over-engineers the solution. Platform engineers should define push topology (which secrets go where, which accounts) via their standard GitOps workflow. holos-console just triggers syncs.
RBAC
Push operations inherit the same RBAC as the underlying secret operation:
- Trigger push: Implicit on
CreateSecretandUpdateSecret— if you can write the secret, the push triggers automatically. - View push status: Requires
PERMISSION_SECRETS_READ(same as reading secret data). Returned viaGetPushStatusRPC. - Manage push associations (add/remove PushSecret references): Requires
PERMISSION_SECRETS_ADMIN(OWNER role). This is a metadata change analogous to updating sharing grants.
Prerequisites (not in scope of this issue)
- ESO installed in the cluster (
external-secrets.ioCRDs available). SecretStoreorClusterSecretStoreconfigured for each target AWS account.PushSecretresources created referencing holos-console-managed K8s Secrets.- Pod Identity configured so ESO can authenticate to AWS.
Implementation Plan
Phase 1: Protobuf Definitions
Extend the SecretsService protobuf with push status support.
Changes to proto/holos/console/v1/secrets.proto:
- Add
GetPushStatusRPC toSecretsService. - Add
PushSecretStatusmessage with fields:name(string),ready(bool),message(string — error detail when not ready),last_synced_unix(int64). - Add
GetPushStatusRequestmessage with fields:project(string),name(string — the K8s Secret name). - Add
GetPushStatusResponsemessage with field:push_secrets(repeatedPushSecretStatus). - Add
push_secretsfield (repeated string) toSecretMetadatato surface associated PushSecret names in list responses. - Add
UpdatePushSecretsRPC,UpdatePushSecretsRequest(project, name, push_secrets repeated string), andUpdatePushSecretsResponsefor managing the association annotation. - Run
make generate.
Phase 2: Push Trigger Mechanism (RED → GREEN)
RED — Write failing tests:
console/secrets/push_test.go: TestTriggerPushSecretsfunction:- When secret has no
push-secretsannotation → no-op, no error. - When secret has
push-secrets: '["ps1","ps2"]'→ patches each PushSecret withforce-syncannotation. - When a referenced PushSecret doesn't exist → logs warning, continues with remaining PushSecrets, returns nil (best-effort).
- Annotation parsing error → logs warning, returns nil (best-effort).
- When secret has no
GREEN — Implement:
-
console/secrets/push.go: New file with:PushSecretsAnnotationconstant:"console.holos.run/push-secrets"ForceSyncAnnotationconstant:"console.holos.run/force-sync"GetPushSecretNames(secret *corev1.Secret) ([]string, error)— parses the annotation.PushClientstruct wrappingdynamic.Interfaceand*resolver.Resolver.NewPushClient(dynamicClient dynamic.Interface, resolver *resolver.Resolver) *PushClient.TriggerPushSecrets(ctx context.Context, project string, pushSecretNames []string) error— patches each named PushSecret withforce-sync: <unix-timestamp>using the dynamic client. Best-effort: logs warnings for missing PushSecrets, does not fail the parent operation.GetPushStatus(ctx context.Context, project string, pushSecretNames []string) ([]*consolev1.PushSecretStatus, error)— reads each PushSecret's.status.conditionsvia the dynamic client. Returns status for each.
-
Wire into Handler: Add
pushClient *PushClientfield (may be nil if dynamic client unavailable). After successfulCreateSecretandUpdateSecret, callTriggerPushSecrets. Best-effort: log and continue if push trigger fails — never fail the secret write because of a push trigger error. -
Wire into
console.go: Createdynamic.NewForConfig(restConfig)alongside the existingkubernetes.NewForConfig(restConfig). Pass toPushClient. PassPushClienttoHandler.
Phase 3: Push Status RPC (RED → GREEN)
RED — Write failing tests:
-
console/secrets/handler_test.go: TestGetPushStatusRPC:- Returns
UNAUTHENTICATEDwithout claims. - Returns
INVALID_ARGUMENTwithout project or name. - Returns
PERMISSION_DENIEDwithout read access. - Returns empty list when secret has no push-secrets annotation.
- Returns status for each referenced PushSecret.
- Returns
ready: falsewith message when PushSecret not found.
- Returns
-
console/secrets/handler_test.go: TestUpdatePushSecretsRPC:- Returns
UNAUTHENTICATEDwithout claims. - Returns
PERMISSION_DENIEDwithout admin access. - Updates the
push-secretsannotation on the K8s Secret. - Validates PushSecret names are non-empty strings.
- Returns
GREEN — Implement:
-
GetPushStatushandler method: Extract push-secrets annotation from the K8s Secret, callpushClient.GetPushStatus, return results. RBAC: requirePERMISSION_SECRETS_READ. -
UpdatePushSecretshandler method: Update thepush-secretsannotation on the K8s Secret. RBAC: requirePERMISSION_SECRETS_ADMIN(OWNER). -
Populate
push_secretsinSecretMetadata: InbuildSecretMetadata, read the annotation and populate thepush_secretsstring list so the UI gets this data fromListSecrets.
Phase 4: UI Integration
-
SecretPage — Push status indicator: Below the secret name/description area, show a "Push Status" section when
metadata.push_secretsis non-empty. For each PushSecret, display:- Name
- Status chip: green "Synced" when ready, yellow "Pending" when not ready and no error, red "Error" with message when not ready with error.
- Last synced timestamp (relative, e.g., "2 minutes ago").
- Fetch status via
GetPushStatusRPC on page load and after save.
-
SecretPage — Push secrets management: For OWNER users, add an "External Sync" section (or integrate into the existing sharing panel) where they can:
- View associated PushSecret names.
- Add/remove PushSecret references via
UpdatePushSecretsRPC. - Input validation: PushSecret name must be a valid K8s resource name.
-
SecretsListPage — Push indicator: In the secrets list table, add a small sync icon/indicator column showing whether the secret has push targets and their aggregate status (all synced = green, any error = red, otherwise = yellow).
Phase 5: Final Cleanup
- Scan for dead code, unused imports, stale comments introduced by the implementation.
- Update
AGENTS.mdarchitecture section to document the push integration. - Verify
make generate && make test && make lintall pass. - Remove any TODO comments that were resolved during implementation.
TODO
- Phase 1: Protobuf definitions
- Phase 2: Push trigger mechanism
- Phase 3: Push status RPC
- Phase 4: UI integration
- Phase 5: Final cleanup