Skip to content

feat(agent): add SMS/Conjur JWT authentication alongside username/password - #817

Draft
roeezis wants to merge 5 commits into
jetstack:masterfrom
roeezis:public/CP-21164-sms-conjur-jwt
Draft

feat(agent): add SMS/Conjur JWT authentication alongside username/password#817
roeezis wants to merge 5 commits into
jetstack:masterfrom
roeezis:public/CP-21164-sms-conjur-jwt

Conversation

@roeezis

@roeezis roeezis commented Jul 1, 2026

Copy link
Copy Markdown

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-jwt for a short-lived Conjur access token, and presents it as a Bearer credential. Only the token source changes, behind the existing identity.RequestAuthenticator seam; dataupload / service discovery / upload pipeline untouched.

Auth-method selection (backward compatibility)

config.cyberark.serviceId set?           -> Conjur JWT exchange      [preferred]
else ARK_USERNAME + ARK_SECRET present?  -> legacy username/password [GA installs]
else                                     -> fail closed at startup
both set                                 -> serviceId wins (warn, ignore UP)

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, sets Bearer. Implements RequestAuthenticator. Requests the base64-encoded access token explicitly (Conjur's canonical wire form); it is forwarded as an opaque credential.
  • internal/cyberark/jwtsource/file source (projected SA token, default /var/run/secrets/tokens/jwt); spiffe deferred.
  • internal/cyberark/identity/username_password.go — legacy UP-login retained, behaviour unchanged.
  • internal/cyberark/client.goselectAuthenticator picks the method by config. account="conjur" confirmed correct (was a POC placeholder — verified against a live tenant).
  • internal/cyberark/servicediscovery/ — parse the secrets_manager service from service discovery into a new Services.SecretsManager field, 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-requires service_id; runtime fails closed if no method configured.
  • helm — optional ARK_USERNAME/ARK_SECRET env alongside the projected aud=conjur token volume; README + values document selection. config.cyberark.serviceId must be the bare authenticator service-id segment, not the full policy path.

The authn-jwt exchange targets secrets_manager, not identity_administration

Worth a reviewer's attention, since it is the one non-obvious wiring detail here. authn-jwt/<service-id>/<account>/authenticate is served by Secrets Manager, which service discovery reports as a different host from identity_administration:

identity_administration  https://<identity-id>.id.<env>-cyberark.cloud
secrets_manager          https://<subdomain>.secretsmgr.<env>-cyberark.cloud/api

An earlier revision of this branch passed Identity.API as the Conjur base URL, so every live exchange hit a host that does not serve the endpoint. Fixed by resolving secrets_manager explicitly and failing closed when it is absent. The username/password path still uses Identity.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 point Identity and SecretsManager at deliberately distinct hosts, and discovery_test.go asserts the two differ, so reading the wrong one fails.

Onboarding

Tenant onboarding — the authn-jwt authenticator 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, gofmt clean, helm lint clean), 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

roeezisholz and others added 5 commits July 1, 2026 12:57
…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
roeezis force-pushed the public/CP-21164-sms-conjur-jwt branch from 750483d to c17b5bf Compare August 10, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants