magdev 242eeca238 feat: add cross-invocation passphrase caching via Linux keyring (v0.3.0)
Use keyctl (keyutils) to cache the master passphrase in the kernel keyring
with a configurable TTL (default 5 min). New unlock/lock subcommands for
manual cache control. keyctl is optional — silently skipped if not installed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 05:04:07 +01:00

hubmanager

A Bash CLI tool to manage Docker Registry images remotely. Supports Docker Hub and any self-hosted Docker Registry v2 API, with flexible authentication.

Requirements

  • Bash 4.0+
  • curl
  • jq
  • openssl (optional — required only when using encrypted config values)
  • keyctl (optional — from keyutils; enables passphrase caching across invocations)

Installation

# System-wide
sudo install -m 755 hubmanager /usr/local/bin/hubmanager

# User-local
install -m 755 hubmanager ~/bin/hubmanager

Quick Start

# Test your credentials and save them
hubmanager login --registry https://registry.example.com \
    --user admin --password secret --save

# Save with password encrypted at rest
hubmanager login --registry https://registry.example.com \
    --user admin --password secret --save --encrypt

# List all repositories
hubmanager list

# List tags for an image
hubmanager tags myuser/myapp

# Inspect an image
hubmanager inspect myuser/myapp:latest

# Delete an old tag
hubmanager delete myuser/myapp:v1.0.0

# Prune old tags (keep the 3 most recent)
hubmanager prune myuser/myapp --keep 3 --dry-run

Configuration

Credentials and registry settings are stored in ~/.config/hubmanager.conf. The file uses a simple KEY=VALUE format:

# ~/.config/hubmanager.conf
# chmod 600 ~/.config/hubmanager.conf

# Default registry and credentials
REGISTRY=https://registry.example.com
USERNAME=admin
PASSWORD=mysecretpassword

# Named registry aliases (use with --registry <alias>)
REGISTRY_STAGING_URL=https://staging-registry.example.com
REGISTRY_STAGING_USERNAME=deploy
REGISTRY_STAGING_PASSWORD=deploytoken

REGISTRY_HUB_URL=https://registry-1.docker.io
REGISTRY_HUB_USERNAME=myuser
REGISTRY_HUB_PASSWORD=myhubtoken

The file must be readable only by the owner (chmod 600). hubmanager will warn if permissions are too open.

Named aliases let you switch registries with a short name:

hubmanager --registry staging list
hubmanager --registry hub tags myuser/myapp

Encrypted credentials

Use login --save --encrypt to store the password encrypted with AES-256-CBC. A master passphrase is prompted at save time (with confirmation) and on every subsequent invocation that reads the config file. Requires openssl.

hubmanager login --registry https://registry.example.com \
    --user admin --password secret --save --encrypt
# New master passphrase: ****
# Confirm master passphrase: ****
# Login Succeeded — bearer auth, registry: https://registry.example.com
# Credentials saved to /home/user/.config/hubmanager.conf
# Password stored encrypted (AES-256-CBC). Master passphrase required on each use.

The config file stores an enc: prefixed ciphertext:

REGISTRY=https://registry.example.com
USERNAME=admin
PASSWORD=enc:U2FsdGVkX1+...base64ciphertext...

The enc: prefix also works for named alias passwords (REGISTRY_<ALIAS>_PASSWORD). On every command that reads the config, the master passphrase is prompted once and cached for the duration of the session.

Passphrase caching across invocations

If keyctl (from the keyutils package) is installed, the master passphrase is automatically cached in the Linux kernel keyring for 5 minutes. Subsequent commands within that window will not re-prompt.

# First command prompts for passphrase, caches it for 5 min
hubmanager list

# Runs without prompting (within cache window)
hubmanager tags myuser/myapp

# Pre-cache before a scripted batch
hubmanager unlock
hubmanager list && hubmanager tags myuser/myapp && hubmanager inspect myuser/myapp:latest

# Clear cache immediately
hubmanager lock

# Custom timeout (10 minutes)
hubmanager unlock --cache-timeout 600

If keyctl is not installed, passphrase caching is silently skipped — each invocation prompts as before.

Global Options

hubmanager [OPTIONS] <command> [COMMAND OPTIONS]

  -r, --registry <url>    Registry base URL
                          Default: https://registry-1.docker.io
  -u, --user <username>   Username (overrides config file)
  -p, --password <pass>   Password or token (overrides config file)
      --config <file>     Config file path (default: ~/.config/hubmanager.conf)
      --cache-timeout <s> Passphrase cache TTL in seconds (default: 300)
      --json              Output raw JSON (pipe-friendly)
      --no-color          Disable ANSI color
  -v, --verbose           Show HTTP request details (with auth redacted)
  -q, --quiet             Suppress all non-error output
  -h, --help              Show help
      --version           Show version

Commands

login — Test and save credentials

hubmanager login [--registry URL] [--user USER] [--password PASS] [--save] [--encrypt]

Validates credentials against the registry. Use --save to write them to the config file. Add --encrypt to store the password encrypted with AES-256-CBC (requires openssl).

hubmanager login --registry https://registry.example.com \
    --user admin --password secret --save
# Login Succeeded — bearer auth, registry: https://registry.example.com
# Credentials saved to /home/user/.config/hubmanager.conf

unlock — Cache master passphrase

hubmanager unlock [--cache-timeout SECONDS]

Prompts for the master passphrase and stores it in the Linux kernel keyring for the configured timeout (default: 300 seconds / 5 minutes). Useful before running a batch of commands. Requires keyctl (keyutils package).

hubmanager unlock
# hubmanager master passphrase: ****
# Passphrase cached for 300s.

# Custom timeout
hubmanager unlock --cache-timeout 600

lock — Clear cached passphrase

hubmanager lock

Immediately revokes the cached passphrase from the kernel keyring.

hubmanager lock
# Passphrase cache cleared.

list — List repositories

hubmanager list [--limit N] [--last REPO]

Lists all repositories in the registry. On Docker Hub, lists repositories for the authenticated user (Docker Hub restricts the _catalog endpoint).

hubmanager list
# REPOSITORY
# myuser/myapp
# myuser/myapi
# myuser/nginx-custom

# Paginate
hubmanager list --limit 50 --last myuser/myapi

# JSON output
hubmanager list --json | jq '.repositories[]'

tags — List tags for an image

hubmanager tags <image> [--limit N] [--last TAG]
hubmanager tags myuser/myapp
# TAG
# latest
# v1.2.3
# v1.2.2
# develop

# Official Docker Hub images
hubmanager tags nginx

# JSON
hubmanager tags myuser/myapp --json | jq '.tags[]'

inspect — Show image details

hubmanager inspect <image>:<tag|digest> [--platform OS/ARCH]

Shows manifest digest, size, OS/arch, creation date, labels, and layer breakdown.

hubmanager inspect myuser/myapp:latest
# Image:            myuser/myapp:latest
# Digest:           sha256:abc123...
# MediaType:        application/vnd.docker.distribution.manifest.v2+json
# CompressedSize:   32.7 MB (34299597 bytes)
# OS/Arch:          linux/amd64
# Created:          2024-01-15T10:30:00.000000000Z
# Labels:
#   maintainer=dev@example.com
#   version=1.2.3
# Layers:           3
#   [0] sha256:1111... (27.8 MB)
#   [1] sha256:2222... (4.4 MB)
#   [2] sha256:3333... (1000.0 KB)

# Multi-arch image — shows all platforms
hubmanager inspect nginx:latest

# Multi-arch — drill into a specific platform
hubmanager inspect nginx:latest --platform linux/arm64

# Inspect by digest
hubmanager inspect myuser/myapp@sha256:abc123...

# JSON output (includes _digest field)
hubmanager inspect myuser/myapp:latest --json | jq .

delete — Delete a tag or manifest

hubmanager delete <image>:<tag|digest> [--yes]

Deletes a manifest by resolving the tag to its content-addressable digest, then issuing a DELETE. Requires REGISTRY_STORAGE_DELETE_ENABLED=true on self-hosted registries. Not supported on Docker Hub.

hubmanager delete myuser/myapp:v1.0.0
# About to delete: myuser/myapp @ sha256:abc123...
# Registry: https://registry.example.com
# Type 'yes' to confirm: yes
# Deleted: myuser/myapp @ sha256:abc123...

# Skip confirmation
hubmanager delete myuser/myapp:v1.0.0 --yes

# Delete by digest directly
hubmanager delete myuser/myapp@sha256:abc123... --yes

copy — Copy or retag an image

hubmanager copy <src-image>:<tag> <dst-image>:<tag> [options]
Options:
  --src-registry URL    Source registry (default: global --registry)
  --dst-registry URL    Destination registry (default: global --registry)
  --src-user USER       Source username
  --src-password PASS   Source password
  --dst-user USER       Destination username
  --dst-password PASS   Destination password
  --platform OS/ARCH    Copy only one platform from a multi-arch image

Same-registry retag — attempts cross-repo blob mount (no data transfer):

hubmanager copy myuser/myapp:v1.2.3 myuser/myapp:stable

Cross-registry copy — streams blobs from source to destination:

hubmanager copy myuser/myapp:latest \
    --src-registry https://registry-1.docker.io \
    --dst-registry https://registry.example.com \
    mycompany/myapp:latest

Copy specific platform from a multi-arch image:

hubmanager copy nginx:latest myuser/nginx-amd64:latest --platform linux/amd64

prune — Delete outdated tags

hubmanager prune <image> [options]

Options:
  --keep N             Number of recent tags to keep (default: 3)
  --older-than DAYS    Delete tags older than N days (overrides --keep)
  --exclude PATTERN    Extended regex of tags to never delete
                       Default: "^(latest|stable|main|master|release)$"
  --no-exclude         Disable the default exclusion pattern
  -n, --dry-run        Show what would be deleted without deleting
  -y, --yes            Skip confirmation prompt

Tags are sorted by image creation date (newest first). The newest N are kept; the rest are deleted. Tags matching the exclusion pattern are always preserved.

# Preview: keep 5 most recent, protect default tags
hubmanager prune myuser/myapp --keep 5 --dry-run

# Keep 3 most recent, auto-confirm
hubmanager prune myuser/myapp --keep 3 --yes

# Delete anything older than 30 days
hubmanager prune myuser/myapp --older-than 30 --dry-run

# Custom exclusion: never delete latest or any semver tag
hubmanager prune myuser/myapp --keep 5 \
    --exclude "^(latest|v[0-9]+\.[0-9]+(\.[0-9]+)?)$"

# Prune everything (no exclusions)
hubmanager prune myuser/myapp --keep 1 --no-exclude --dry-run

Authentication

hubmanager automatically detects the authentication method by probing the registry's /v2/ endpoint:

Registry type Auth method
Docker Hub Bearer tokens via auth.docker.io
Harbor, self-hosted with token server Bearer tokens via registry-configured realm
Basic-auth self-hosted HTTP Basic Auth on every request
Public/anonymous registries No auth

Bearer tokens are cached in memory for the duration of the session and refreshed automatically when they expire.

Docker Hub notes

  • list uses the Docker Hub REST API (hub.docker.com) because the _catalog endpoint is restricted on Docker Hub.
  • delete is not supported via the v2 API on Docker Hub. Use the web UI at https://hub.docker.com.
  • prune is not supported on Docker Hub for the same reason.

Cleaning up empty repositories

The Docker Registry v2 API does not provide an endpoint to delete repositories. After deleting all tags from a repository, it will still appear in hubmanager list. This is a limitation of the registry spec, not of hubmanager.

To remove empty repositories, you need direct access to the registry host:

# 1. Delete the repository directory from storage
rm -rf /var/lib/registry/docker/registry/v2/repositories/<repo-name>

# 2. Run garbage collection to reclaim disk space
docker exec <registry-container> registry garbage-collect /etc/docker/registry/config.yml

# 3. Restart the registry so the catalog refreshes
docker restart <registry-container>

JSON output

All commands support --json for machine-readable output:

# Get all tags as a JSON array
hubmanager tags myapp --json | jq '.tags'

# Get digest of latest
hubmanager inspect myapp:latest --json | jq '._digest'

# Delete and capture result
hubmanager delete myapp:old --yes --json
# {"deleted":true,"digest":"sha256:..."}

# Prune and capture counts
hubmanager prune myapp --keep 3 --yes --json
# {"deleted":5,"failed":0}

Exit Codes

Code Meaning
0 Success
1 General / usage error
2 Authentication failure
3 Resource not found (404)
4 Permission denied (403)
5 Registry server error (5xx)
6 Network / connectivity error
7 Operation not supported by registry

Examples

# Mirror all tags of an image to a private registry
for tag in $(hubmanager tags nginx --json | jq -r '.tags[]'); do
    hubmanager copy nginx:$tag \
        --src-registry https://registry-1.docker.io \
        --dst-registry https://registry.example.com \
        mycompany/nginx:$tag
done

# List images older than 60 days (dry run)
hubmanager prune myuser/myapp --older-than 60 --dry-run

# Get the SHA256 digest of the production image
DIGEST=$(hubmanager inspect myuser/myapp:production --json | jq -r '._digest')
echo "Production image: $DIGEST"

# Promote staging image to production (retag)
hubmanager copy myuser/myapp:staging myuser/myapp:production

Configuration reference

Key Description
REGISTRY Default registry URL
USERNAME Default username
PASSWORD Default password or token; prefix with enc: for encrypted values
CACHE_TIMEOUT Passphrase keyring cache TTL in seconds (default: 300)
REGISTRY_<ALIAS>_URL URL for a named registry alias
REGISTRY_<ALIAS>_USERNAME Username for a named alias
REGISTRY_<ALIAS>_PASSWORD Password for a named alias (supports enc: prefix)

Aliases are case-insensitive and treat - as _. For example, alias my-reg maps to REGISTRY_MY_REG_URL.

Description
Bash CLI tool for managing Docker Registry images remotely. The tool supports both Docker Hub and self-hosted Docker Registry v2 API, with authentication via config file and/or CLI flags.
Readme 57 KiB
Languages
Shell 100%