Skip to content

Add private-registry mount convention for source-scan dependency auth (#400) - #417

Merged
haksungjang merged 5 commits into
mainfrom
feature/400-private-registry-auth-mount
Sep 7, 2026
Merged

Add private-registry mount convention for source-scan dependency auth (#400)#417
haksungjang merged 5 commits into
mainfrom
feature/400-private-registry-auth-mount

Conversation

@haksungjang

Copy link
Copy Markdown
Contributor

Summary

Adds the infrastructure half of #400: a /etc/trusca/registry mount convention (settings.xml / .npmrc / pip.conf / .netrc) so an operator can authenticate cdxgen's Maven/npm/pip dependency resolution against a private registry during a source scan. This is a separate feature from ER3's container-image registry credentials, which authenticate Trivy pulling an image.

  • docker-compose.yml / docker-compose.dev.yml: REGISTRY_CONFIG_HOST_PATH (default ./secrets/registry, same pattern as COSIGN_KEYS_HOST_PATH) mounted read-only at the fixed path /etc/trusca/registry on the scan-pipeline worker only (worker-scan / celery-worker), plus MVN_ARGS / NPM_CONFIG_USERCONFIG / PIP_CONFIG_FILE passthrough vars in the x-backend-env anchor. A commented-out second mount line covers .netrc, which has no pointer variable and must land at $HOME/.netrc.
  • charts/trustedoss/: new worker.scan.extraEnv / extraVolumes / extraVolumeMounts, scoped to the scan Deployment alone (unlike the chart-wide env.extraVolumes used for the private-CA example) since that is the only workload that runs cdxgen.
  • docs-site/docs/admin-guide/private-registries.md (+ KO mirror): full walkthrough, cross-linked from the Scans page's container-image "Private registries" section to disambiguate the two features. reference/env-variables.md (+ KO) documents the four new keys.
  • CHANGELOG.md entry, .gitignore entries mirroring the cosign-key pattern.

Maven mechanism (verified, not assumed)

The tracking issue's "Direction" note suggested a MAVEN_SETTINGS variable. That variable does not exist - Maven itself only reads ~/.m2/settings.xml or a -s/--settings CLI flag, and Maven has no environment-variable form of that flag.

I checked cdxgen's actual mechanism against its installed source (@cyclonedx/cdxgen@12.2.0, the repo pins 12.3.3):

  • lib/cli/index.js:1526-1528 and lib/helpers/utils.js:13210-1613: cdxgen reads process.env.MVN_ARGS, splits it on spaces, and appends it to the mvn argument list it builds itself.
  • lib/cli/index.js:1550-1554: when cdxgen finds a settings.xml next to a pom.xml it is scanning, its own console output says: "maven settings.xml found in ${basePath}. Please set the MVN_ARGS environment variable ... Example: MVN_ARGS='--settings ${settingsXml}'".
  • lib/stages/pregen/envAudit.js:162: MVN_ARGS is also in cdxgen's own JVM-option-injection audit list, alongside GRADLE_ARGS/JAVA_TOOL_OPTIONS, confirming it as a real, first-class cdxgen input.

So MVN_ARGS=--settings /etc/trusca/registry/settings.xml is the actual mechanism, and it is cdxgen's own feature, not something this PR invents. NPM_CONFIG_USERCONFIG (npm) and PIP_CONFIG_FILE (pip) are each tool's own documented env vars - cdxgen does nothing special for them beyond normal subprocess env inheritance when it shells out to npm install / pip install.

Security notes (for security-reviewer)

  • Fixed mount path. /etc/trusca/registry is a literal in docker-compose.yml / the Helm templates, never interpolated from a project setting, API field, or scan-trigger parameter - it cannot become an arbitrary-file-read via user input. Only REGISTRY_CONFIG_HOST_PATH (the host side) is operator-configurable, same as the existing COSIGN_KEYS_HOST_PATH pattern.
  • Read-only. Both compose mounts and the Helm extraVolumeMounts example are :ro / readOnly: true.
  • Scoped to the one workload that needs it. The Helm mechanism (worker.scan.*) is new and deliberately narrower than the existing chart-wide env.extraVolumes (used for the private-CA example): a registry credential has no reason to be readable from backend/beat/worker-default, which never run cdxgen. Docker Compose mounts it on worker-scan/celery-worker only, mirroring the existing cosign-key mount's scoping.
  • .netrc footgun called out explicitly. Docker auto-creates a missing bind-mount source as an empty directory, which would silently shadow $HOME/.netrc for every tool that reads it if an operator uncommented that line without creating the file first. Both compose files' comments and the doc page call this out before showing the line.
  • Not yet reaching the scan workspace archive or SBOM metadata by construction, since the mount lives outside /workspace and cdxgen only walks the cloned source tree there - but I have not audited the full archive/tar code path myself (out of scope: tasks/scan_source.py is excluded from this PR). Flagging for security-reviewer to confirm.
  • scan.error_message leak risk (referenced F-1 precedent): I did not touch any error-message-construction code (out of scope, apps/backend/**), so there is nothing in this PR that could leak these paths into it, but the follow-up backend change (below) should re-check this when it lands.

What still needs a backend-developer follow-up

integrations/_subprocess_env.py is out of my scope as devops-engineer (apps/backend/**), so the mount and passthrough env vars in this PR do not yet reach the cdxgen subprocess - _CDXGEN_EXTRA_ALLOWLIST needs three new entries added:

_CDXGEN_EXTRA_ALLOWLIST: frozenset[str] = frozenset(
    {
        ...
        "MVN_ARGS",
        "NPM_CONFIG_USERCONFIG",
        "PIP_CONFIG_FILE",
    }
)

I confirmed locally that apps/backend/tests/unit/test_config_key_contract.py (the .env.example/reference/infrastructure parity oracle) passes cleanly both before and after this PR, so no test currently enforces that these three are forwarded - a new integration test exercising scrubbed_env_for_cdxgen() with the allowlist entries in place (following the existing test patterns in that file) should land with the backend change.

Refs #400. The offline install bundle (#400-4~6) is out of scope for this cycle.

Verification

$ python3 -c "import yaml; yaml.safe_load(open('docker-compose.yml')); yaml.safe_load(open('docker-compose.dev.yml'))"
OK (docker-compose config -q itself is blocked in this sandbox environment)

$ helm lint --strict charts/trustedoss -f charts/trustedoss/ci/scan-values.yaml
1 chart(s) linted, 0 chart(s) failed

$ helm template test charts/trustedoss -f charts/trustedoss/ci/scan-values.yaml -f <registry-example-values> -s templates/deployment-worker-scan.yaml
# renders MVN_ARGS env + registry-config volume/mount on worker-scan only, confirmed absent from backend/beat/worker-default

$ node tools/em-dash/lint.mjs
em-dash: OK, no em dash added in changes since origin/main.

$ node tools/ko-style/lint.mjs --all --fail-on S2
ko-style: 104 file(s), 0 finding(s) (S1 0 · S2 0 · S3 0).

$ REDIS_URL=redis://localhost:6379/13 DATABASE_URL=postgresql+asyncpg://... python3 -m pytest apps/backend/tests/unit/test_config_key_contract.py apps/backend/tests/unit/test_prod_failclosed_secrets_compose_wiring.py apps/backend/tests/unit/test_compose_log_rotation.py -q
14 passed

@haksungjang

Copy link
Copy Markdown
Contributor Author

Added the missing backend half: `MVN_ARGS` and `PIP_CONFIG_FILE` are now in `_CDXGEN_EXTRA_ALLOWLIST` (apps/backend/integrations/subprocess_env.py), plus a test exercising `scrubbed_env_for_cdxgen()` for all three variables. `NPM_CONFIG_USERCONFIG` needed no code change — it already passes through the existing `npm_config` prefix band, confirmed by the same test so a future narrowing of that band doesn't silently break it.

The mount this PR adds is now functionally complete end to end: file lands in `/etc/trusca/registry/`, environment variable points cdxgen's subprocess at it, cdxgen forwards it to the underlying `mvn`/`npm`/`pip` invocation.

…#400)

Source scans have no way to authenticate cdxgen against a private
Maven/npm/pip registry (container-image registry auth, ER3, is a
separate feature). Operators can now drop settings.xml/.npmrc/pip.conf/
.netrc under REGISTRY_CONFIG_HOST_PATH (default ./secrets/registry),
mounted read-only at a fixed /etc/trusca/registry on the scan-pipeline
worker in both compose files, with a matching Helm worker.scan.extraEnv/
extraVolumes/extraVolumeMounts scoped to that Deployment alone.

Maven's actual mechanism is cdxgen's own MVN_ARGS variable, not
MAVEN_SETTINGS (which does not exist) - verified against
@cyclonedx/cdxgen's source (lib/cli/index.js, lib/helpers/utils.js) and
its own console guidance. NPM_CONFIG_USERCONFIG and PIP_CONFIG_FILE are
npm's/pip's own documented env vars, inherited via normal subprocess
env passthrough.

This lands the mount convention, compose/Helm wiring, and docs
(admin-guide/private-registries.md, EN+KO). The env vars still need to
be added to integrations/_subprocess_env.py's cdxgen allowlist before
they reach the cdxgen subprocess - out of scope for this PR (apps/backend
is owned by backend-developer); see the PR description for the exact
follow-up.
The private-registry mount this PR adds is inert without this: the
files land on disk but cdxgen's subprocess never sees a pointer to
them. Maven has no MAVEN_SETTINGS env var; cdxgen reads MVN_ARGS
itself and appends it to the mvn invocation it builds (confirmed
against @cyclonedx/cdxgen's source, not assumed). PIP_CONFIG_FILE is
pip's own documented variable. NPM_CONFIG_USERCONFIG needed no change:
it already passes through the existing npm_config_ prefix band.

Both new entries are file paths, not credentials, so they are
unaffected by _looks_like_credential.
The settings.xml guidance relied on a bare <server> entry, which Maven
matches to a repository by <id> rather than <url> - a malicious pom.xml
in the scanned project can declare a <repository> with a matching <id>
and redirect the credential to an attacker URL. Add a <mirror
mirrorOf="*"> example that pins resolution to one trusted URL instead,
and document the risk under "Known limits".

The .npmrc guidance did not show host-scoped credential lines. Add a
//host/:_authToken= example and warn against unscoped _auth/_authToken,
which npm would send to any registry a scanned project's package.json
names via publishConfig.registry.

Updates the admin guide (EN + KO) and the matching values.yaml comment.
…sages

cdxgen shells into pip/npm/mvn for dependency resolution, and a failed
request against a scheme://user:pass@host private-registry URL can echo
that URL, credential included, into stderr. run_cdxgen copied stderr
verbatim into CdxgenFailed, which the scan pipeline's generic failure
handler stores as-is in scan.error_message, an API-exposed field any team
member can read.

Move the credential-shaped-substring scrubber already used for the live
scan-log stream (tasks/_progress.py) into a shared integrations module so
integrations/cdxgen.py can reuse it without a tasks -> integrations
layering violation. Apply the same scrub to scancode's identical failure
path and to the lockfile-prep failure log, which share the same shape.
@haksungjang
haksungjang force-pushed the feature/400-private-registry-auth-mount branch from 0236a83 to 326c845 Compare September 7, 2026 01:59
@haksungjang
haksungjang merged commit 43a58b0 into main Sep 7, 2026
32 checks passed
@haksungjang
haksungjang deleted the feature/400-private-registry-auth-mount branch September 7, 2026 02:41
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