Skip to content

feat: add Connect/Workbench PPM authenticated repos via Identity Federation#114

Open
ian-flores wants to merge 15 commits intomainfrom
ppm-authenticated-repos
Open

feat: add Connect/Workbench PPM authenticated repos via Identity Federation#114
ian-flores wants to merge 15 commits intomainfrom
ppm-authenticated-repos

Conversation

@ian-flores
Copy link
Collaborator

Description

Enable Connect and Workbench to authenticate against PPM using Kubernetes Identity Federation (RFC 8693 token exchange). When authenticatedRepos: true is set on a product spec, the operator automatically injects init container + sidecar pods that exchange K8s projected service account tokens for PPM API tokens, writing netrc/curlrc files for Python and R package installation.

Issue

  • PTDC-223: Configure Connect to automatically use PPM authenticated repos
  • PTDC-222: Configure Workbench to automatically use PPM authenticated repos

Dependencies

Code Flow

Authentication Flow

K8s Projected SA Token → Init Container → PPM /__api__/token (RFC 8693) → PPM API Token
                                                                                │
                                                                        Written to netrc + curlrc
                                                                                │
                         Connect/Workbench ← reads netrc/curlrc ←───────────────┘
                         (R: CURL_HOME, Python: NETRC env var)

Sidecar refreshes the token on a timer (default 50min for 60min token lifetime).

Key Components

  1. PPM Identity Federation Config (package_manager_config.go): PackageManagerIdentityFederationConfig struct with full field coverage. Extended GenerateGcfg() to emit named [IdentityFederation "connect"] sections.

  2. Token Exchange Helpers (ppm_auth.go): Shared functions for init container, sidecar container, volumes, volume mounts, and env vars. Shell script (mounted from ConfigMap) uses wget + sed (BusyBox built-ins in alpine:3) for zero-dependency operation.

  3. Product Integration (connect.go, workbench.go): When AuthenticatedRepos=true, adds projected SA token volume, shared emptyDir, script ConfigMap volume, init container, sidecar container, NETRC + CURL_HOME env vars.

  4. Site Controller (site_controller.go): Creates {site}-ppm-auth-script ConfigMap when any product has authenticated repos enabled. Auto-configures Identity Federation entries on PPM based on product flags.

Testing

Tested end-to-end on ganso01-staging with adhoc image adhoc-ppm-auth-repos-v1.16.1-15-gedc6aaa.

Test Status
Connect pods 3/3 Running (connect + chronicle + ppm-auth-sidecar)
Workbench pods 3/3 Running (workbench + chronicle + ppm-auth-sidecar)
Connect init container exchanges SA token for PPM API token
Workbench init container exchanges SA token for PPM API token
/mnt/ppm-auth/netrc contains valid JWT with correct subject claim
/mnt/ppm-auth/.curlrc contains --netrc-file directive
NETRC and CURL_HOME env vars set on main product containers
PPM auth_available: true and sso_enabled: true after enabling repo auth

Category of change

  • New feature (non-breaking change which adds functionality)

Enable Connect and Workbench to authenticate against PPM using
Kubernetes Identity Federation (RFC 8693 token exchange). Adds
OIDC and Identity Federation config types for PPM, shared token
exchange init container and sidecar helpers, and opt-in
AuthenticatedRepos flag on Connect/Workbench specs.
All 8 findings addressed. Build passes and tests pass. Here's the summary:
Changes:
- Install curl and jq in token exchange script (`apk add --no-cache`) so alpine:3 default image works
- Add null/empty token validation after jq extraction to fail fast instead of writing "null" as password
- Add `OIDCAudience` field to `SiteSpec` so OIDC audience is configurable (defaults to `sts.amazonaws.com` for backwards compatibility)
- Revert `AutomountServiceAccountToken` to `ptr.To(false)` in Connect — projected volume works independently
- Add `PPMAuthImage` to `InternalConnectSpec` and `InternalWorkbenchSpec` and propagate from Site controllers
- Add gcfg injection validation for `IdentityFederation` Name (reject `"`, `]`, newlines)
- Add `cleanupPPMAuthConfigMap` to delete the ConfigMap when authenticated repos feature is disabled
- Add `SanitizePPMUrl` helper to strip existing scheme before prepending `https://`, preventing double-prefix
- Fix audience mismatch: thread OIDCAudience from site spec through
  Connect/Workbench to projected SA token volumes
- Remove runtime apk add from token exchange script (image must
  have curl+jq pre-installed)
- Add sidecar resilience: catch refresh failures instead of dying
- Remove unused ppmAuthCurlrcPath constant
- Clarify AutomountServiceAccountToken comment re: projected volumes
- Add .claude/tsc-cache to .gitignore
Add typed support for all PPM authentication configuration fields:
- New [Authentication] section (APITokenAuth, DeviceAuthType, session
  lifetime, etc.)
- 12 new [OpenIDConnect] fields (ClientSecretFile, PKCE, claims
  customization, token lifetime, etc.)
- 9 new [IdentityFederation] fields (claims, groups, roles, logging)
alpine:3 runs as root by default, which fails pod security contexts
that require runAsNonRoot. Set RunAsUser=65534 (nobody) on both init
and sidecar containers.

PPM requires Scope, RoleClaim, or GroupToScopeMapping when OIDC is
configured. Default to "repos:read:*" in the site controller.
alpine:3 doesn't include curl or jq, and apk install requires root.
Replace curl with wget (BusyBox built-in) and jq with a sed-based
JSON field extractor for zero-dependency operation.
The field was tagged json:"-" which prevented it from being stored
in the PackageManager CR. The site controller sets it in memory but
it was lost when the PM controller read the CR back from the API.
The Name field was tagged json:"-" so it was lost during Kubernetes
API serialization. This caused empty section names in the generated
gcfg: [IdentityFederation ""] instead of [IdentityFederation "connect"].
PPM treats ClientSecret as a literal value, not a file path. Use
ClientSecretFile instead so PPM reads the secret from the mounted file.

K8s SA tokens don't have a preferred_username claim. Set UniqueIdClaim
and UsernameClaim to "sub" for Identity Federation entries.
All builds pass and tests are green. Findings #1 (PPMAuthImage propagation), #2 (cache files), and #3 (apk add stderr) were already addressed in prior commits. I fixed the two remaining issues.
Changes:
- `SanitizePPMUrl` now returns empty string for empty input instead of invalid `"https://"`
- Added edge case tests for `SanitizePPMUrl`: empty string and URL with port/path
@claude
Copy link

claude bot commented Mar 5, 2026

Claude finished @ian-flores's task —— View job


Review: PPM Authenticated Repos via Identity Federation

Well-structured feature with good separation of concerns. The auth flow (projected SA token → init container → PPM token exchange → netrc/curlrc) is sound. Here are findings organized by severity.


Security (Elevated Scrutiny)

1. SA token visible in process argsppm_auth.go:53
The shell script passes the SA token via wget --post-data="...subject_token=${SA_TOKEN}...", which means the full token is visible in /proc/<pid>/cmdline. Consider writing the post data to a temp file and using --post-file instead.

2. Error logging leaks response datappm_auth.go:60
On failure, the script logs "Response was: $RESPONSE" to stderr. If PPM returns error details containing internal information, this ends up in container logs. Consider truncating or omitting the raw response.

3. extract_json_field sed regex is fragileppm_auth.go:45
The sed-based JSON parser won't handle escaped quotes in values. While PPM access tokens (JWTs) won't contain literal quotes, this is a latent risk if the pattern is reused for other fields.


Validation Gaps

4. Missing validation: AuthenticatedRepos=true without PPMUrlconnect.go:595, workbench.go:754
When AuthenticatedRepos is true but PPMUrl is empty, SanitizePPMUrl("") returns "", and the init container will wget to /__api__/token — failing cryptically. Add a validation guard here or at the API level.

5. OIDCIssuerURL empty with AuthenticatedRepos=truesite_controller_package_manager.go:139
If AuthenticatedRepos is enabled on Connect/Workbench but OIDCIssuerURL is empty, no Identity Federation entries are created on PPM, yet product pods still have the auth sidecar trying to exchange tokens. This will fail at runtime with no operator-level warning. Consider logging a warning or adding validation.

6. EKS-specific default audiencesite_controller_package_manager.go:142
Defaulting audience to "sts.amazonaws.com" is EKS-specific. AKS users would get silent misconfiguration. Consider making both OIDCIssuerURL and OIDCAudience required when authenticated repos are enabled, or at minimum logging when the default is used.


Code Quality

7. Unused parameter ppmURLppm_auth.go:175
PPMAuthVolumes(siteName, ppmURL, audience string) accepts ppmURL but never uses it in the function body. Remove to avoid confusion.

8. Manual field serialization for IdentityFederationpackage_manager_config.go:118-170
The field-by-field serialization is brittle — if a new field is added to PackageManagerIdentityFederationConfig, this code must be updated in lockstep. The main loop uses reflection for other sections. Could the same approach work here, with just the section header [IdentityFederation "name"] being special-cased?

9. Inconsistent JSON tag casingpackage_manager_config.go:20
IdentityFederation uses lowercase json:"identityFederation" while all other fields in PackageManagerConfig use capitalized tags matching gcfg section names ("Server", "Http", etc.). Was this intentional?

10. Duplicated PPM auth setup logicconnect.go:589-606 and workbench.go:748-765
The PPM auth setup blocks in Connect and Workbench reconcilers are nearly identical. Consider extracting to a shared helper that returns all five slices, reducing duplication.


API Design

11. PPMUrl and PPMAuthAudience on product specsconnect_types.go:158, workbench_types.go:120
These fields exist on both ConnectSpec and WorkbenchSpec, but when used via the Site controller, they're derived from site-level config. These are only meaningful for standalone CRs. Consider documenting this distinction or adding validation.


Minor

12. Cleanup doesn't check ownershipsite_controller.go:522
cleanupPPMAuthConfigMap deletes the ConfigMap without checking the ManagedByLabelKey label. If a user created a ConfigMap with the same name, it would be deleted.


What looks good

  • Non-root security context on init/sidecar containers (RunAsUser: 65534, AllowPrivilegeEscalation: false)
  • Atomic netrc writes via temp file + rename
  • Resource limits on sidecar container
  • Sidecar resilience (token refresh failures don't crash the loop)
  • Good test coverage for the helper functions
  • IdentityFederation name validation against injection chars (", ], newlines)
  • CRDs and Helm chart are in sync

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of PPM authenticated repos feature — see the main Claude comment for detailed findings.

@ian-flores ian-flores marked this pull request as ready for review March 5, 2026 18:52
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.

1 participant