feat(agent): add SMS/Conjur JWT authentication alongside username/password - #817
Draft
roeezis wants to merge 5 commits into
Draft
feat(agent): add SMS/Conjur JWT authentication alongside username/password#817roeezis wants to merge 5 commits into
roeezis wants to merge 5 commits into
Conversation
…sword Adds a workload-identity auth path: the agent reads a projected ServiceAccount token, exchanges it at Secrets Manager SaaS (Conjur Cloud) authn-jwt for a short-lived Conjur access token, and presents it as a Bearer credential. The existing CyberArk Identity username/password login is retained; the method is selected automatically by config: config.cyberark.serviceId set -> Conjur JWT exchange (preferred) else ARK_USERNAME + ARK_SECRET -> username/password (unchanged) both set -> serviceId wins neither -> fail closed Only the token source changes, behind the existing identity.RequestAuthenticator seam; dataupload, service discovery and the upload pipeline are untouched. - internal/cyberark/conjur: authn-jwt exchange, ~8m token cache, Bearer - internal/cyberark/jwtsource: file source (projected SA token); spiffe deferred - internal/cyberark/identity: username/password login retained (split into username_password.go), behaviour unchanged - internal/cyberark/client.go: selectAuthenticator picks the method; NewRequestAuthenticator exposed for envelope/keyfetch - internal/envelope/keyfetch: JWKS fetch uses the same authenticator selector - pkg/agent/config.go: config.cyberark.* keys; service_id no longer mandatory - deploy/charts/disco-agent: projected token volume (aud=conjur), config keys, optional ARK_USERNAME/ARK_SECRET; docs Unit tests cover the four selection cases and the retained username/password login.
… base64 token Adds hack/onboard-disco-agent.sh: full onboarding for the DisCo agent's SMS/Conjur JWT auth, tenant-level (branch, uploader group, value-less authorization-probe secret) and per-cluster (authenticator, workload, grants). Runs entirely with the customer's own Conjur Cloud admin token; DisCo holds no Conjur identity of its own. Idempotent, fails closed, supports --dry-run/--skip-deploy. Also: account="conjur" is confirmed correct (was a POC placeholder), and the authn-jwt exchange now requests the base64-encoded access token explicitly (Conjur's canonical wire form), matching what the DisCo authorizer expects.
…/README Cross-PR review caught real bugs missed by unit tests on both sides: - onboard-disco-agent.sh created per-cluster branches at data/<cluster-UUID>, but the authorizer's agent_provider_from_identity() expects host/data/<provider_type>/<provider_id>/workloads/<sub>. Real onboarded agents produced an identity the authorizer's own parser rejects. Fixed by adding a data/<type> branch before the cluster branch (now C1-C7, was C1-C6); verified end-to-end against the real authorizer parser. - templates/configmap.yaml never rendered the cyberark: block into the agent's config.yaml, despite values.yaml/values.schema.json declaring those fields — Conjur-JWT auth was silently unreachable via this chart. - TestConfig_CyberArk_Validation had two subtests asserting a config-time "service_id is required" error that no longer exists (validation moved to runtime, ErrNoAuthMethod). Updated to assert the current contract. - README's onboarding section documented a stale v1 policy-file mechanism that doesn't match what the v2-API script does, and never mentioned the script exists. Rewrote to point at hack/onboard-disco-agent.sh. Also fixed a self-contradicting serviceId example in both the README table and values.schema.json (prose said "bare segment", the example showed the full policy path).
selectAuthenticator passed serviceMap.Identity.API as the Conjur base URL, but authn-jwt/<service-id>/<account>/authenticate is served by Secrets Manager (Conjur Cloud), not by identity_administration. On the integration test tenant those are entirely different hosts: identity_administration https://<identity-id>.id.<env>-cyberark.cloud secrets_manager https://<subdomain>.secretsmgr.<env>-cyberark.cloud/api so every live token exchange hit a host that does not serve the endpoint. Verified against the tenant: the exchange now returns 200 and the agent completes gather -> minimize -> upload. The unit tests could not catch this. FakeCyberArk and the auth-selection tests wired the Conjur exchange mock *into* Identity.API, so the mock followed whichever field the code happened to read; both the bug and the fix passed. The mocks now point Identity and SecretsManager at deliberately distinct hosts, so reading the wrong one fails. - servicediscovery: parse the secrets_manager service into a new Services.SecretsManager field, following the existing discoverycontext pattern; make the endpoint a template field in the testdata fixture. - client.go: read SecretsManager.API for the Conjur exchange and fail closed with a named error when it is absent. The username/password path still uses Identity.API, unchanged. - discovery_test.go: assert secrets_manager is parsed into SecretsManager and that it differs from Identity, so this cannot regress silently. Drive-by, needed to compile and run the suite locally: - conjur/mock.go took *testing.T while every sibling mock in internal/cyberark takes testing.TB, so it could not be called from pkg/testutil's testing.TB helper. - two TestConfig_CyberArk_Validation subtests bound ValidateAndCombineConfig's return values in the wrong order and read .CyberArk off the client instead of the config.
Tenant onboarding is a UI flow in the CyberArk console, not something customers run a script for, so the script does not belong in this repo. Removes hack/onboard-disco-agent.sh and rewrites the chart README's onboarding section to state what onboarding must produce (an authn-jwt authenticator scoped to the cluster's issuer/JWKS, a registered workload for the agent's ServiceAccount, and the grants to authenticate and upload) and to point at the product documentation for the walkthrough. Also removes references to server-side implementation details from code comments — specific source files and internal service names — leaving the protocol-level facts a reader of this repo needs: the authn-jwt exchange is served by Secrets Manager rather than identity_administration, and the returned base64 token is forwarded as an opaque Bearer credential. No functional change; the agent's auth path is untouched.
roeezis
force-pushed
the
public/CP-21164-sms-conjur-jwt
branch
from
August 10, 2026 14:58
750483d to
c17b5bf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add a workload-identity JWT auth path to the agent, alongside the existing CyberArk Identity username/password login — not replacing it. Both methods coexist and the agent auto-selects by config. The agent reads a projected ServiceAccount token, exchanges it at Secrets Manager (Conjur Cloud)
authn-jwtfor a short-lived Conjur access token, and presents it as aBearercredential. Only the token source changes, behind the existingidentity.RequestAuthenticatorseam;dataupload/ service discovery / upload pipeline untouched.Auth-method selection (backward compatibility)
serviceId-wins lets a migrating install add the service-id before removing old creds — no rollout window with no method. Existing GA installs (UP creds, no serviceId) keep working with zero config change.
What changed
internal/cyberark/conjur/— authn-jwt exchange, short-lived token cache, setsBearer. ImplementsRequestAuthenticator. Requests the base64-encoded access token explicitly (Conjur's canonical wire form); it is forwarded as an opaque credential.internal/cyberark/jwtsource/—filesource (projected SA token, default/var/run/secrets/tokens/jwt);spiffedeferred.internal/cyberark/identity/username_password.go— legacy UP-login retained, behaviour unchanged.internal/cyberark/client.go—selectAuthenticatorpicks the method by config.account="conjur"confirmed correct (was a POC placeholder — verified against a live tenant).internal/cyberark/servicediscovery/— parse thesecrets_managerservice from service discovery into a newServices.SecretsManagerfield, and use it as the base URL for the authn-jwt exchange. See the note below.internal/envelope/keyfetch/— JWKS fetch uses the same authenticator selector.pkg/agent/config.go— MachineHub validation no longer hard-requiresservice_id; runtime fails closed if no method configured.ARK_USERNAME/ARK_SECRETenv alongside the projectedaud=conjurtoken volume; README + values document selection.config.cyberark.serviceIdmust be the bare authenticator service-id segment, not the full policy path.The authn-jwt exchange targets
secrets_manager, notidentity_administrationWorth a reviewer's attention, since it is the one non-obvious wiring detail here.
authn-jwt/<service-id>/<account>/authenticateis served by Secrets Manager, which service discovery reports as a different host fromidentity_administration:An earlier revision of this branch passed
Identity.APIas the Conjur base URL, so every live exchange hit a host that does not serve the endpoint. Fixed by resolvingsecrets_managerexplicitly and failing closed when it is absent. The username/password path still usesIdentity.API, unchanged.This class of bug was invisible to the unit tests: the mocks wired the Conjur exchange server into
Identity.API, so the mock followed whichever field the code happened to read and both the bug and the fix passed. The mocks now pointIdentityandSecretsManagerat deliberately distinct hosts, anddiscovery_test.goasserts the two differ, so reading the wrong one fails.Onboarding
Tenant onboarding — the
authn-jwtauthenticator scoped to the cluster's OIDC issuer and JWKS, a registered workload for the agent's ServiceAccount, and the grants that let it authenticate and upload — is performed through the CyberArk web console and is out of scope for this repo. The chart README states what onboarding must produce and points at the product documentation.Onboarding needs only the tenant administrator's own credentials. The agent holds no Conjur identity beyond its projected ServiceAccount token, and nothing in this chart requires a Conjur admin credential at deploy time.
Tests
Full suite green locally (
go build ./...,go test ./...with envtest assets,gofmtclean,helm lintclean), including the 4 auth-selection cases (serviceID→conjur, UP→identity, both→conjur wins, neither→error), the retained UP-login tests, and the new service-discovery assertion described above.Validated end-to-end against a live Secrets Manager tenant: the authn-jwt exchange returns a token and the agent completes gather → minimize → upload.
🤖 Generated with Claude Code