Skip to content

Conversation

@JAORMX
Copy link
Collaborator

@JAORMX JAORMX commented Dec 4, 2025

Summary

  • Adds support for injecting authentication files (npmrc, netrc, yarnrc) into container builds for protocol schemes (npx://, uvx://, go://)
  • Solves the problem that NPM registry-scoped authentication cannot be configured via environment variables because the format (//registry.example.com/:_authToken=TOKEN) contains characters that are invalid as environment variable names
  • Files are injected into the builder stage only and not included in the final container image for security
  • Auth file content is stored securely in ToolHive's encrypted secrets provider (keyring or encrypted file), never in plain text config files

Large PR Justification

This PR is 1000+ lines but the changes are cohesive and cannot be easily split:

Category Files Lines Description
Core logic buildauthfile.go ~95 Config markers + secret name generation
Config integration config.go, interface.go ~100 Provider interface methods
CLI commands config_buildauthfile.go ~300 3 new subcommands with secrets integration
Templates npx.tmpl, uvx.tmpl, go.tmpl ~15 Conditional COPY for auth files
Build integration protocol.go ~100 Resolve secrets + write auth files during build
Unit tests buildauthfile_test.go ~300 Comprehensive test coverage
Generated mock_provider.go, CLI docs ~300 Auto-generated code

The feature requires all components to work together - splitting would result in non-functional intermediate PRs.

New CLI Commands

# Set npmrc for private npm registry (stored securely in secrets)
thv config set-build-auth-file npmrc '//npm.corp.example.com/:_authToken=TOKEN'

# Set netrc for pip/Go authentication
thv config set-build-auth-file netrc 'machine github.com login git password TOKEN'

# Read from stdin (recommended - avoids exposing secrets in shell history)
cat ~/.npmrc | thv config set-build-auth-file npmrc --stdin
thv config set-build-auth-file npmrc --stdin < ~/.npmrc

# List configured auth files (credentials hidden by default)
thv config get-build-auth-file

# Show with content (retrieves from secrets)
thv config get-build-auth-file npmrc --show-content

# Remove specific file (also removes from secrets)
thv config unset-build-auth-file npmrc

# Remove all files
thv config unset-build-auth-file --all

Supported Auth Files

File Container Path Use Case
npmrc ~/.npmrc npm/npx private registries
netrc ~/.netrc pip, Go modules, curl authentication
yarnrc ~/.yarnrc Yarn private registries

Guide: Setting Up Private NPM Registry Authentication

Prerequisites

Before configuring auth files, ensure ToolHive secrets are set up:

thv secret setup

This enables secure storage using your system keyring or encrypted file storage.

Step 1: Configure the Registry URL

Tell ToolHive to use your private registry instead of the public npmjs.org:

thv config set-build-env NPM_CONFIG_REGISTRY https://npm.corp.example.com

Note: Use host.containers.internal instead of localhost if your registry runs locally, as builds happen inside containers.

Step 2: Configure Authentication

NPM requires registry-scoped authentication tokens in .npmrc format. Set your auth token (stored securely in secrets, not config):

# Option 1: From stdin (recommended - avoids shell history)
cat ~/.npmrc | thv config set-build-auth-file npmrc --stdin

# Option 2: From file redirect
thv config set-build-auth-file npmrc --stdin < ~/.npmrc

# Option 3: Direct argument (visible in shell history - use with caution)
thv config set-build-auth-file npmrc '//npm.corp.example.com/:_authToken=YOUR_TOKEN_HERE'

For registries requiring basic auth or multiple registries:

thv config set-build-auth-file npmrc --stdin <<'EOF'
//npm.corp.example.com/:_authToken=TOKEN1
//npm.other.example.com/:_authToken=TOKEN2
always-auth=true
EOF

Step 3: Verify Configuration

# Check build environment variables
thv config get-build-env

# Check auth files (content hidden by default)
thv config get-build-auth-file

# Verify secret was created
thv secret list  # Should show BUILD_AUTH_FILE_npmrc

# Verify with dry-run (shows generated Dockerfile)
thv build 'npx://@myorg/my-private-package' --dry-run

The dry-run output should show:

  • ENV NPM_CONFIG_REGISTRY="https://npm.corp.example.com"
  • COPY .npmrc /root/.npmrc

Step 4: Run Your Private Package

thv run 'npx://@myorg/my-private-package' --name my-server

Security Notes

  • Credentials stored encrypted - Auth file content is stored in ToolHive's secrets provider (keyring or encrypted file), never in plain text config
  • Config stores markers only - The config file only stores secret:BUILD_AUTH_FILE_npmrc, not actual credentials
  • Credentials stay in builder stage only - The .npmrc file is NOT copied to the final container image
  • Credentials hidden by default - Use --show-content flag to display actual token values
  • Use --stdin for secrets - Avoids exposing credentials in shell history and process listings
  • Multi-stage builds - Authentication happens during npm install in the builder stage; the runtime image only contains the installed packages

Cleanup

# Remove specific settings (also removes from secrets)
thv config unset-build-env NPM_CONFIG_REGISTRY
thv config unset-build-auth-file npmrc

# Or remove all auth files at once
thv config unset-build-auth-file --all

Test plan

  • Unit tests for buildauthfile.go functions
  • Linter passes
  • All unit tests pass
  • E2E tested with local Verdaccio registry:
    • Published private scoped package @test/mcp-hello
    • Configured registry URL and auth token
    • Successfully built container from npx://@test/mcp-hello
    • Verified package was fetched from private registry (not npmjs.org)
    • Verified credentials are NOT in final container image
    • Verified credentials stored in secrets, not plain text config

🤖 Generated with Claude Code

@github-actions github-actions bot added the size/L Large PR: 600-999 lines changed label Dec 4, 2025
@codecov
Copy link

codecov bot commented Dec 4, 2025

Codecov Report

❌ Patch coverage is 32.08955% with 91 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.25%. Comparing base (0c0d907) to head (2520f8d).

Files with missing lines Patch % Lines
pkg/runner/protocol.go 7.57% 60 Missing and 1 partial ⚠️
pkg/config/interface.go 0.00% 30 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2909      +/-   ##
==========================================
- Coverage   56.36%   56.25%   -0.11%     
==========================================
  Files         323      324       +1     
  Lines       31764    31898     +134     
==========================================
+ Hits        17904    17945      +41     
- Misses      12331    12423      +92     
- Partials     1529     1530       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/L Large PR: 600-999 lines changed labels Dec 4, 2025
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Large PR Detected

This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.

How to unblock this PR:

Add a section to your PR description with the following format:

## Large PR Justification

[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformation

Alternative:

Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.

See our Contributing Guidelines for more details.


This review will be automatically dismissed once you add the justification section.

@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Dec 4, 2025
@github-actions github-actions bot dismissed their stale review December 4, 2025 21:57

Large PR justification has been provided. Thank you!

@github-actions
Copy link
Contributor

github-actions bot commented Dec 4, 2025

✅ Large PR justification has been provided. The size review has been dismissed and this PR can now proceed with normal review.

@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Dec 4, 2025
Copy link
Contributor

@jhrozek jhrozek left a comment

Choose a reason for hiding this comment

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

I will admit that this was an LLM finding but it seems important. Otherwise the pR looks good to me.

JAORMX and others added 4 commits December 4, 2025 19:02
Add support for injecting authentication files (npmrc, netrc, yarnrc)
into container builds for protocol schemes (npx://, uvx://, go://).

This solves the problem that NPM registry-scoped authentication cannot
be configured via environment variables because the format
(//registry.example.com/:_authToken=TOKEN) contains characters that
are invalid as environment variable names.

Key features:
- New CLI commands: set-build-auth-file, get-build-auth-file, unset-build-auth-file
- Files are injected into the builder stage only and NOT included in
  the final container image for security
- Supports npmrc (npm/npx), netrc (pip/Go), and yarnrc (Yarn)
- Credentials are hidden by default in CLI output (use --show-content
  to display)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
This avoids exposing secrets in shell history and process listings.

Examples:
  cat ~/.npmrc | thv config set-build-auth-file npmrc --stdin
  thv config set-build-auth-file npmrc --stdin < ~/.npmrc

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Refactor build auth file storage to use ToolHive's encrypted secrets
provider instead of storing credentials in plain text config files.

- Config now stores only markers (e.g., "secret:BUILD_AUTH_FILE_npmrc")
- Actual content stored in keyring/encrypted secrets provider
- CLI commands updated to use secrets manager for storage/retrieval
- protocol.go resolves secrets at build time
- Updated interface methods and all provider implementations

This ensures sensitive credentials like NPM tokens are never stored
in plain text on disk.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@JAORMX JAORMX force-pushed the feat/build-auth-files branch from 4e5ebb5 to 2e24d46 Compare December 5, 2025 01:02
@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Dec 5, 2025
…image

When building from a local path (e.g., go://./myapp), the build context
is copied to /build/ which may include auth files like .netrc, .npmrc,
or .yarnrc. The final stage then copies /build/ to /app/, which would
leak these credentials to the final container image.

Add cleanup steps after the build completes (so auth was used during
package fetching) but before the final stage copies from /build/:

- go.tmpl: RUN rm -f /build/.netrc /build/.npmrc /build/.yarnrc /root/.netrc
- npx.tmpl: RUN rm -f /build/.netrc /build/.npmrc /build/.yarnrc /root/.npmrc
- uvx.tmpl: RUN rm -f /build/.netrc /build/.npmrc /build/.yarnrc /root/.netrc

This only affects local path builds (IsLocalPath=true). Remote package
builds (e.g., npx://@org/package) are unaffected since they only copy
specific files like node_modules or package.json.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@JAORMX
Copy link
Collaborator Author

JAORMX commented Dec 5, 2025

Thanks for catching this @jhrozek! Good find - I've added cleanup steps to all three templates (go, npx, uvx) in commit 7f9ddf7.

The issue only affects local path builds (e.g., go://./myapp) where the entire build context gets copied to /build/ and then to the final image. Remote package builds are unaffected since they only copy specific files.

The fix adds RUN rm -f /build/.netrc /build/.npmrc /build/.yarnrc /root/.netrc after the build completes (so auth was used during package fetching) but before the final stage copies from /build/.

@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Dec 5, 2025
@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Dec 5, 2025
@slyt3
Copy link

slyt3 commented Dec 5, 2025

Hey, quick security issue or more like concern about the fix, removing files with rm -f so doesnt actualy remove them from docker layers?

because docker layers are immutable if I am not wrong, so when you do
``dockerfile
COPY .npmrc /root/.npmrc
RUN npm install
RUN rm -f /root/.npmrc


but the most important thing that first layer can still be extracted with `docker save` plus + tar. haha

so you could use BuildKit instead

```dockerfile
RUN --mount=type=secret,id=npmrc,dst=/root/.npmrc \
    npm install

so you build with

docker build --secret id=npmrc,src=.npmrc .

so its secret is never ever stored and never stored in any layer, so its temprorarily during the RUN command

@JAORMX
Copy link
Collaborator Author

JAORMX commented Dec 6, 2025

Yeah, this is mostly an issue for local path builds. Protocol builds with packages (the most common scenario) use multi-stage builds, and the secret is only persisted on the initial and not the end container image.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR: 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants