AgentCredential Controller¶
Overview¶
The AgentCredential controller gives each Target an identity in the SolAr
cluster for its solar-agent. It is active only when --agent-apiserver-url is
set. The code is in pkg/controller/agentcredential_controller.go and
pkg/controller/agentcredential.go.
For how the credential reaches the agent, see Target Controller and the RenderTask controller. For the operator and user side, see Setting up the solar-agent.
Objects¶
All objects live in the Target's namespace and are removed by the finalizer. Only the TargetReport has an owner reference to the Target, because both live in SolAr's etcd. The others live in the SolAr cluster and have none: if SolAr's etcd were emptied, the garbage collector would otherwise delete every agent identity in the fleet.
| Object | Name | Content |
|---|---|---|
| ServiceAccount | solar-agent-<target> |
|
| Role | solar-agent-<target> |
get on targets and get, update on targetreports, both limited by resourceNames to <target> |
| RoleBinding | solar-agent-<target> |
Role to ServiceAccount |
| TargetReport | <target> |
Empty spec, created only if missing |
| Secret | solar-agent-<target>-<n> |
Key token; labels solar.opendefense.cloud/agent-credential: <target> and solar.opendefense.cloud/agent-credential-target-uid: <target UID>; annotations rotation-request and expires-at |
RBAC cannot restrict create to one name, so the controller pre-creates the
TargetReport. The agent has no create permission. An empty report rolls up
to PreflightReady=Unknown, so the pre-created report does not mark the Target
healthy.
<n> is one more than the highest suffix among the Target's credential Secrets,
starting at 1. The newest Secret that holds a token is the current credential.
Only Secrets with both labels count, so a Target re-created under the same name
never picks up a Secret of its predecessor. Such leftovers exist only if the
predecessor's finalizer was bypassed. Before minting, the controller deletes
every Secret with the Target's name label but a different (or missing) UID
label: their tokens would still work for the reused ServiceAccount, and their
names would block minting. Revoke and the finalizer match on the name label
alone.
Credential Secrets are read through an uncached reader. A stale cache could otherwise hide a Secret that was just created and cause a second mint.
Finalizer¶
solar.opendefense.cloud/agent-credential-finalizer is set on the Target. On
deletion the controller deletes the credential Secrets, RoleBinding, Role,
ServiceAccount and TargetReport, then removes the finalizer. When the feature is
off, no finalizer is added.
Lifecycle¶
stateDiagram-v2
[*] --> Identity: Target created
Identity --> Minted: no credential Secret
Minted --> Current: token stored, Issued
Current --> Minted: rotate annotation or less than 20% lifetime left
Current --> Cleanup: report Revision ends with -credentialTag
Cleanup --> Current: older Secrets deleted
Current --> Revoked: annotation agent-credential=revoked
Revoked --> Minted: annotation removed
- Mint. The controller ensures ServiceAccount, Role, RoleBinding and
TargetReport. If there is no credential, or the newest has no token, or a
rotation is due, it creates Secret
<n>and requests a token for the ServiceAccount. The token is bound to that Secret and has an explicitexpirationSecondsfrom--agent-token-ttl. Without it TokenRequest would default to one hour. The token andexpires-atare written to the Secret. If the TokenRequest or the update fails, the Secret is deleted again, so an empty newest Secret is never rendered. - Rotate. A new value in
solar.opendefense.cloud/rotate-agent-credentialthat no Secret has answered (annotationrotation-request) triggers a mint. Less than 20% of lifetime remaining triggers one too. The controller requeues at that point. - Cleanup. Older Secrets are deleted only after the
TargetReportshows a release whoseRevisionends in-<credentialTag>of the current Secret. Both tokens authenticate as the same ServiceAccount, so reports alone cannot prove the switch. Until then the old token keeps working. - Revoke. With
solar.opendefense.cloud/agent-credential: revokedthe controller deletes all credential Secrets and mints none. It keeps the ServiceAccount, Role and RoleBinding. The bound token dies with its Secret. Revocation is checked right after deletion handling, before theDisabledcheck and before any identity is ensured, so it holds even when credentials are disabled. Removing the annotation mints a fresh credential.
Conditions¶
AgentCredentialReady on the Target:
| Status | Reason | When |
|---|---|---|
True |
Issued |
A current credential exists |
False |
Disabled |
--agent-apiserver-url is empty |
False |
Revoked |
The revoke annotation is set |
False |
MintFailed |
Creating the identity or the token failed |
Why the tag hashes the Secret UID¶
credentialTag is a short hash of the Secret's UID, and the Target controller
appends it to the release chart tag. It does not use the Secret name for two
reasons:
- The release chart repository is per namespace, so two Targets in one namespace share it. Their credentials must give different tags.
- A Secret name is reused after a revoke. A repeated tag would make the renderer skip the push and ship the old, revoked token.
The Target controller also puts a hash of the API server URL and CA before it, so a changed connection renders a new chart too.
A new tag also changes the bootstrap input, so the bootstrap re-renders and Flux picks up the new chart.
Watches¶
| Watched Resource | Mapping |
|---|---|
Target |
Direct reconcile |
Secret |
The Target named by the solar.opendefense.cloud/agent-credential label |
TargetReport |
The Target with the same name. Updates pass only when a release Revision changed, since each reconcile lists Secrets uncached |
RBAC¶
The controller needs the rights it hands out, because Kubernetes prevents
privilege escalation when creating a Role. It also needs create on
serviceaccounts/token and write access to Secrets. The markers are in
agentcredential_controller.go; make manifests regenerates the ClusterRole.