Files
at-container-registry/docs/CREDENTIAL_HELPER.md
T
Evan Jarrett 6758996300 add SBOM package diffing, verify hold-service captain records
- diff view gains a Packages tab with added/removed/changed/unchanged
  package tables and purl-derived type/license/upstream links
- captain records verified against the DID's atcr_hold service before
  caching (processor + batch backfill), preventing forged holds
- fix empty-handle updates clobbering cached handles and colliding on
  the UNIQUE constraint
- move fillPrevCIDs into repo.go; DirectRepoOperator is now canonical,
  repomgr kept as a test oracle
- surface read-only crew status in hold selector
- reconcile docs
2026-06-13 12:49:03 -05:00

16 KiB

Distribution Strategy for ATCR Credential Helper

Overview

The ATCR credential helper is distributed as pre-built binaries for Linux, macOS, and Windows. Builds are produced with GoReleaser and the resulting artifacts are published to the project's Tangled repository as ATProto records (sh.tangled.repo.artifact) on the repo owner's PDS. There is no GitHub release pipeline. This document describes the actual distribution flow.

Why Go is Ideal for Credential Helpers

Go is a natural fit for Docker credential helpers:

  1. Cross-compilation - Single command builds for all platforms
  2. Static binaries - No runtime dependencies (unlike Node.js, Python, Ruby)
  3. Industry standard - Most Docker credential helpers are written in Go:
    • docker-credential-helpers (official)
    • docker-credential-gcr (Google)
    • docker-credential-ecr-login (AWS)
    • docker-credential-pass (community)

Multi-Brand Structure

The credential helper is built as multiple brand-specific binaries from a single shared implementation:

  • pkg/credhelper/ holds the entire implementation (Docker protocol commands, device-flow auth, config storage, self-update). It exposes credhelper.Run(credhelper.Config{...}).

  • cmd/credential-helper/atcr/ and cmd/credential-helper/seamark/ are thin main packages (each with its own go.mod) that supply a per-brand Config and call Run. They differ only in brand identity:

    Field atcr binary seamark binary
    Binary name docker-credential-atcr docker-credential-seamark
    Default registry atcr.io seamark.cr
    Config dir (under $HOME) ~/.atcr ~/.seamark
    Secret prefix atcr_device_ seamark_device_

    Both brands point ReleasesBaseURL at the same Tangled repo (https://tangled.org/did:plc:e3kzdezk5gsirzh7eoqplc64) for self-update and download.

Each brand module is independently installable via go install (see From Source). The atcr module path is atcr.io/cmd/credential-helper/atcr; the seamark module path is seamark.dev/cmd/credential-helper/seamark.

Supported Platforms

Platform Arch Format Status
Linux amd64 tar.gz Available
Linux arm64 tar.gz Available
macOS amd64 (Intel) tar.gz Available
macOS arm64 (Apple Silicon) tar.gz Available
Windows amd64 tar.gz Available
Windows arm64 tar.gz Available

Distribution Methods

1. Tangled Releases (Automated)

Trigger: Push a version tag (e.g. v1.0.0).

git tag v1.0.0
git push origin v1.0.0

What happens:

  1. The Tangled CI workflow .tangled/workflows/release-credential-helper.yml runs on tags matching v*.
  2. It installs goat (ATProto CLI) and goreleaser, then logs into the repo owner's PDS once with goat account login.
  3. goreleaser release --clean builds binaries for all platforms.
  4. GoReleaser's release block is disabled (release.disable: true), so no GitHub/forge release is created. Instead, a custom publishers block runs ./scripts/publish-artifact.sh for each built archive and the checksums file.
  5. publish-artifact.sh uploads each artifact as a PDS blob (goat blob upload) and creates an sh.tangled.repo.artifact record referencing it. The record uses a deterministic rkey derived from (tag, artifact name) so retries are idempotent (Tangled's PDS returns HTTP 500, not 409, on duplicate rkey).

Workflow file: .tangled/workflows/release-credential-helper.yml Config file: .goreleaser.yaml Publisher script: scripts/publish-artifact.sh

The artifacts become downloadable from the Tangled repo's tag download path (see Manual Download).

2. Install Scripts

Both scripts are served by the AppView from its static directory (pkg/appview/public/static/) at /static/install.sh and /static/install.ps1. They resolve the latest version by following the {repo}/tags/latest redirect chain on Tangled, then download the matching archive from the tag download path.

Linux/macOS: install.sh

  • Detects OS and architecture
  • Resolves the latest tag from Tangled and downloads the archive
  • Installs to /usr/local/bin (override with INSTALL_DIR)
  • Makes executable and verifies installation

Windows: install.ps1

  • Detects architecture
  • Resolves the latest tag from Tangled and downloads the archive
  • Installs to %ProgramFiles%\ATCR (override with ATCR_INSTALL_DIR)
  • Adds to system PATH (requires Administrator to modify the machine PATH)
  • Uses the bundled tar.exe to extract the .tar.gz

Usage:

# Linux/macOS
curl -fsSL https://atcr.io/static/install.sh | bash

# Windows (PowerShell)
iwr -useb https://atcr.io/static/install.ps1 | iex

Pin a specific version by setting ATCR_VERSION (e.g. ATCR_VERSION=v1.0.0) before running either script.

3. Homebrew (macOS) — Not Currently Available

Homebrew distribution is not available yet. A brews: block exists in .goreleaser.yaml but is commented out. If/when it is enabled, the formula would live in the project's Tangled repo under Formula/ and pull artifacts from the Tangled tag download path. There is no published tap to brew tap today; use one of the other methods.

4. Manual Download

Download the archive directly from the Tangled repo's tag download path. The artifacts are published as sh.tangled.repo.artifact records on the repo owner's PDS and served by Tangled at:

https://tangled.org/<did-or-handle>/<repo>/tags/<version>/download/<artifact>
# Example: Linux amd64
REPO=https://tangled.org/did:plc:e3kzdezk5gsirzh7eoqplc64
VERSION=v1.0.0
curl -LO "${REPO}/tags/${VERSION}/download/docker-credential-atcr_${VERSION#v}_Linux_x86_64.tar.gz"
tar -xzf docker-credential-atcr_${VERSION#v}_Linux_x86_64.tar.gz
sudo install -m 755 docker-credential-atcr /usr/local/bin/

Tangled redirects DID to handle for the repo URL, so curl -L is needed to follow the redirect.

5. From Source

For users with Go installed. Each brand is a separate installable module:

# atcr.io brand
go install atcr.io/cmd/credential-helper/atcr@latest
sudo mv "$(go env GOPATH)/bin/atcr" /usr/local/bin/docker-credential-atcr

# seamark.dev brand
go install seamark.dev/cmd/credential-helper/seamark@latest
sudo mv "$(go env GOPATH)/bin/seamark" /usr/local/bin/docker-credential-seamark

The installed binary takes the name of the leaf package directory (atcr / seamark); rename it to docker-credential-<brand> so Docker can discover it.

Note: This requires Go 1.26+ and compiles locally. Locally (inside the repo workspace) go.work resolves the atcr.io dependency; standalone installs resolve the require atcr.io vX.Y.Z line pinned in each brand's go.mod.

Release Process

Creating a New Release

  1. Bump the pinned atcr.io version in each brand's go.mod (cmd/credential-helper/atcr/go.mod, cmd/credential-helper/seamark/go.mod) if go install consumers need the new code.

  2. Commit and tag:

    git add .
    git commit -m "Release v1.0.0"
    git tag -a v1.0.0 -m "Release v1.0.0"
    git push origin main
    git push origin v1.0.0
    
  3. Wait for CI:

    • The Tangled workflow builds and publishes artifacts automatically.
  4. Verify release:

    • Confirm the tags/latest redirect resolves to the new tag and the archive downloads:
      ATCR_VERSION=v1.0.0 curl -fsSL https://atcr.io/static/install.sh | bash
      docker-credential-atcr version
      

Version Information

GoReleaser injects version info at build time via ldflags into the brand main packages:

var (
    version = "dev"       // Set to tag (e.g., "v1.0.0")
    commit  = "none"      // Set to git commit hash
    date    = "unknown"   // Set to build timestamp
)

Usage:

$ docker-credential-atcr version
docker-credential-atcr v1.0.0 (commit: abc123, built: 2025-01-15T10:30:00Z)

Distribution Configuration

GoReleaser Config (.goreleaser.yaml)

Key sections:

Builds:

  • One credential-helper build with dir: ./cmd/credential-helper/atcr, binary name docker-credential-atcr (Windows: .exe auto-added)
  • Targets: Linux, macOS, Windows (amd64, arm64)
  • CGO disabled for static binaries
  • Ldflags inject version/commit/date

Archives:

  • Format: tar.gz for all platforms
  • Naming: docker-credential-atcr_VERSION_OS_ARCH.tar.gz
  • Includes: LICENSE, README, INSTALLATION

Release:

  • release.disable: true — no GitHub/forge release is created.

Publishers:

  • A custom atproto-pds publisher runs ./scripts/publish-artifact.sh for each artifact, forwarding the Tangled-provided TANGLED_REF_NAME, TANGLED_REPO_DID, and REPO_URL env vars (GoReleaser publishers run in a sanitized sub-shell, so these must be forwarded explicitly).

Brews:

  • Present but commented out (Homebrew not currently enabled).

Changelog:

  • Auto-generated from commits
  • Filters out docs/test/chore commits

Docker and Docker Desktop

How Docker Finds Credential Helpers

Docker looks for binaries named docker-credential-* in PATH:

  1. User types: docker push atcr.io/alice/app:latest
  2. Docker reads ~/.docker/config.json:
    {
      "credHelpers": {
        "atcr.io": "atcr"
      }
    }
    
  3. Docker looks for docker-credential-atcr in PATH
  4. Calls docker-credential-atcr get with atcr.io on stdin (a plain string, not JSON)
  5. Helper returns credentials as JSON on stdout

The credHelpers map value is the binary-name suffix after docker-credential- (so atcr for docker-credential-atcr). The configure-docker command (and the prompt at the end of login) writes this entry automatically.

PATH Requirements

Linux/macOS:

  • Common locations: /usr/local/bin, /usr/bin, $HOME/.local/bin
  • Check with: which docker-credential-atcr

Windows:

  • Common locations: C:\Windows\System32, %ProgramFiles%\ATCR
  • Check with: where docker-credential-atcr

CI/CD Secrets

Required Tangled Secret

  • PUBLISH_APP_PASSWORD — an ATProto app password for the account that owns the repo's artifact records. The workflow runs goat account login -u "$TANGLED_REPO_DID" -p "$PUBLISH_APP_PASSWORD" once before GoReleaser, and every publish-artifact.sh invocation reuses that session.

There is no GITHUB_TOKEN or HOMEBREW_TAP_TOKEN — the project does not release through GitHub.

Helper Behavior

The helper implements the standard Docker credential helper protocol plus a few user-facing commands. Implementation lives in pkg/credhelper/.

Docker Protocol Commands (hidden)

Called by Docker, not users (pkg/credhelper/protocol.go):

  • get — reads the server URL from stdin, resolves the stored account, validates the device secret against the AppView, and returns {ServerURL, Username, Secret} JSON. If the OAuth session has expired it prints the login URL and fails so Docker re-prompts; on a generic invalid result it removes the bad account.
  • store — reads {ServerURL, Username, Secret} from stdin. Only stores the credential if Secret carries the brand's secret prefix (e.g. atcr_device_); other secrets (e.g. an app password from docker login) are ignored.
  • erase — removes the active (or sole) account for the server URL.
  • list — returns { "host": "username", ... } for all stored registries.

Device-Flow Authentication

login (pkg/credhelper/cmd_login.go, pkg/credhelper/device_auth.go) runs an OAuth-style device authorization flow against the AppView:

  1. POST {appview}/auth/device/code with {"device_name": "<hostname>"}. Response: device_code, user_code, verification_uri, expires_in, interval.
  2. The helper shows the user_code, then opens (or prints) {verification_uri}?user_code=<code> for the user to approve in a browser.
  3. The helper polls POST {appview}/auth/device/token with {"device_code": ...} every interval seconds until expires_in. authorization_pending means keep polling; any other error aborts. On success the response carries device_secret, handle, and did.
  4. The account (handle, did, device secret) is saved to the brand config dir, and the user is offered automatic Docker configuration.

Credential Validation

get validates a stored device secret by calling GET {appview}/auth/token?service={appview} with HTTP Basic auth (handle:device_secret) and a 5s timeout (validateCredentials in device_auth.go):

  • 200 → valid.
  • 401 with body {"error":"oauth_session_expired", "login_url": ...} → expired; the user is told to re-login.
  • 401 otherwise → invalid; the account is removed.
  • Network errors or other status codes → treated as valid (don't re-auth on transient server issues).

Other User Commands

  • login [registry] — device-flow auth (default registry from brand Config).
  • logout [registry] — remove a stored account.
  • status — show configured registries and accounts.
  • switch — change the active account for a registry.
  • configure-docker — write credHelpers entries to ~/.docker/config.json for all configured registries.
  • update [--check] — self-update by resolving {ReleasesBaseURL}/tags/latest and downloading the matching archive. get also performs a cached (24h) background update check and prints a notice if a newer version exists.

Testing the Distribution

Before Tagging a Release

  1. Test GoReleaser locally:

    goreleaser build --snapshot --clean
    ls dist/
    
  2. Test specific platform:

    goreleaser build --snapshot --clean --single-target
    ./dist/credential-helper_*/docker-credential-atcr version
    
  3. Test full release without publishing:

    goreleaser release --snapshot --clean --skip=publish
    

After Release

  1. Test install script:

    # Clean install in fresh environment
    docker run --rm -it ubuntu:latest bash
    apt update && apt install -y curl
    curl -fsSL https://atcr.io/static/install.sh | bash
    
  2. Test Docker integration:

    echo '{"credHelpers":{"atcr.io":"atcr"}}' > ~/.docker/config.json
    docker push atcr.io/test/image:latest
    

Future Enhancements

Package Managers

  • Homebrew (enable the commented brews: block in .goreleaser.yaml)
  • .deb / .rpm packages (via GoReleaser nfpm)
  • Arch AUR, Chocolatey, Scoop, Winget

Docker Distribution

The credential helper could also ship as a container image for CI/CD use, but native binaries remain the primary distribution method.

Troubleshooting

"Binary not in PATH"

Symptom: docker push fails with "credential helper not found"

Solution:

# Check if installed
which docker-credential-atcr
# or on Windows:
where docker-credential-atcr

# If not in PATH, add install dir to PATH
export PATH="/usr/local/bin:$PATH"  # Linux/macOS
# or add to ~/.bashrc, ~/.zshrc

"Permission denied"

Symptom: Can't execute binary

Solution:

chmod +x /usr/local/bin/docker-credential-atcr

"Wrong architecture"

Symptom: exec format error or bad CPU type

Solution: Download correct architecture:

  • x86_64/amd64 for Intel/AMD
  • arm64/aarch64 for Apple Silicon, ARM servers

Check with:

uname -m

Documentation

References