You've already forked hubmanager
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>
This commit is contained in:
53
CLAUDE.md
53
CLAUDE.md
@@ -10,6 +10,45 @@ authentication detection (bearer token or HTTP basic auth).
|
||||
|
||||
---
|
||||
|
||||
## What Changed (Session 2026-03-01b)
|
||||
|
||||
### Passphrase caching via Linux keyring (v0.3.0)
|
||||
|
||||
Added cross-invocation master passphrase caching using the Linux kernel keyring
|
||||
(`keyctl` from `keyutils`). `keyctl` is an **optional** dependency — if not
|
||||
installed, behaviour is unchanged (prompt every time).
|
||||
|
||||
**New functions** (Encryption helpers section):
|
||||
|
||||
| Function | Purpose |
|
||||
| --- | --- |
|
||||
| `_keyring_available()` | Returns 0 if `keyctl` is installed |
|
||||
| `_keyring_get()` | Read cached passphrase from user keyring; return 1 if missing/expired |
|
||||
| `_keyring_set(pass, ttl)` | Store passphrase in keyring with TTL |
|
||||
| `_keyring_clear()` | Revoke the cached key |
|
||||
|
||||
**New subcommands**:
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `unlock` | Prompt for passphrase and cache in keyring (useful before scripting) |
|
||||
| `lock` | Clear cached passphrase from keyring immediately |
|
||||
|
||||
**New global option**: `--cache-timeout <seconds>` (default 300 / 5 min).
|
||||
Also configurable via `CACHE_TIMEOUT=<seconds>` in `hubmanager.conf`.
|
||||
|
||||
**New constants**: `HM_KEYRING_KEY`, `HM_KEYRING_DEFAULT_TIMEOUT`.
|
||||
**New global variable**: `HM_CACHE_TIMEOUT`.
|
||||
|
||||
**Modified functions**:
|
||||
|
||||
- `_prompt_master_pass()` — checks keyring before prompting; stores after prompt
|
||||
- `_prompt_set_master_pass()` — stores in keyring after confirmation
|
||||
|
||||
**Version bump**: `0.2.1` → `0.3.0`
|
||||
|
||||
---
|
||||
|
||||
## What Changed (Session 2026-03-01)
|
||||
|
||||
### Bug fixes and improvements (v0.2.1)
|
||||
@@ -105,6 +144,8 @@ Both `load_config()` and `resolve_registry_alias()` detect the prefix and call
|
||||
| `delete` | Resolve tag → digest → `DELETE`; requires confirmation or `--yes` |
|
||||
| `copy` | Copy/retag within or across registries; blob mount for same-registry retag |
|
||||
| `prune` | Delete outdated tags sorted by image creation date |
|
||||
| `unlock` | Cache master passphrase in kernel keyring (requires `keyctl`) |
|
||||
| `lock` | Clear cached master passphrase from keyring |
|
||||
|
||||
### Supporting files
|
||||
|
||||
@@ -126,11 +167,12 @@ set -euo pipefail
|
||||
# --- Output / Formatting helpers ---
|
||||
# --- Dependency check ---
|
||||
# --- Encryption helpers --- _encrypt_value(), _decrypt_value(), _prompt_master_pass()
|
||||
# _keyring_get(), _keyring_set(), _keyring_clear()
|
||||
# --- Config loading ---
|
||||
# --- HTTP helpers --- raw_http(), get_response_header()
|
||||
# --- Authentication --- probe_registry_auth(), get_bearer_token(), make_auth_header()
|
||||
# --- Registry request --- registry_request() ← main HTTP wrapper
|
||||
# --- Subcommands --- cmd_login/list/tags/inspect/delete/copy/prune
|
||||
# --- Subcommands --- cmd_login/unlock/lock/list/tags/inspect/delete/copy/prune
|
||||
# --- Global arg parsing --- parse_global_args()
|
||||
# --- Main dispatcher --- main()
|
||||
main "$@"
|
||||
@@ -184,6 +226,14 @@ main "$@"
|
||||
`HM_MASTER_PASS`).
|
||||
- `openssl` is an optional dependency: not checked at startup, only on first `enc:` encounter.
|
||||
|
||||
10. **Passphrase caching** (v0.3.0):
|
||||
- Uses Linux kernel keyring (`keyctl` from `keyutils`) to cache the master passphrase
|
||||
across invocations with a configurable TTL (default 300s / 5 min).
|
||||
- `_prompt_master_pass()` checks keyring before prompting; stores after prompt.
|
||||
- `keyctl` is optional: if not installed, each invocation prompts as before (no errors).
|
||||
- `unlock` pre-caches the passphrase; `lock` clears it immediately.
|
||||
- TTL configurable via `--cache-timeout <seconds>` flag or `CACHE_TIMEOUT` config key.
|
||||
|
||||
---
|
||||
|
||||
## Global Variables (key ones)
|
||||
@@ -199,6 +249,7 @@ main "$@"
|
||||
| `HM_TOKEN_CACHE["registry\|scope"]` | Cached bearer token |
|
||||
| `HM_TOKEN_EXPIRY["registry\|scope"]` | Token expiry (epoch seconds) |
|
||||
| `HM_MASTER_PASS` | Master passphrase for `enc:` config values (session-cached) |
|
||||
| `HM_CACHE_TIMEOUT` | Keyring cache TTL in seconds (default 300) |
|
||||
| `HM_LAST_HTTP_CODE` | HTTP status of most recent request |
|
||||
| `HM_LAST_HEADERS_FILE` | Temp file path with response headers |
|
||||
| `HM_TMPFILES` | Array of temp files, cleaned up via `trap EXIT` |
|
||||
|
||||
66
README.md
66
README.md
@@ -8,6 +8,7 @@ A Bash CLI tool to manage Docker Registry images remotely. Supports Docker Hub a
|
||||
- **curl**
|
||||
- **jq**
|
||||
- **openssl** *(optional — required only when using encrypted config values)*
|
||||
- **keyctl** *(optional — from `keyutils`; enables passphrase caching across invocations)*
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -107,6 +108,33 @@ The `enc:` prefix also works for named alias passwords (`REGISTRY_<ALIAS>_PASSWO
|
||||
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.
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```text
|
||||
@@ -117,6 +145,7 @@ hubmanager [OPTIONS] <command> [COMMAND OPTIONS]
|
||||
-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)
|
||||
@@ -147,6 +176,42 @@ hubmanager login --registry https://registry.example.com \
|
||||
|
||||
---
|
||||
|
||||
### `unlock` — Cache master passphrase
|
||||
|
||||
```text
|
||||
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).
|
||||
|
||||
```bash
|
||||
hubmanager unlock
|
||||
# hubmanager master passphrase: ****
|
||||
# Passphrase cached for 300s.
|
||||
|
||||
# Custom timeout
|
||||
hubmanager unlock --cache-timeout 600
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `lock` — Clear cached passphrase
|
||||
|
||||
```text
|
||||
hubmanager lock
|
||||
```
|
||||
|
||||
Immediately revokes the cached passphrase from the kernel keyring.
|
||||
|
||||
```bash
|
||||
hubmanager lock
|
||||
# Passphrase cache cleared.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `list` — List repositories
|
||||
|
||||
```text
|
||||
@@ -442,6 +507,7 @@ hubmanager copy myuser/myapp:staging myuser/myapp:production
|
||||
| `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) |
|
||||
|
||||
104
hubmanager
104
hubmanager
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# hubmanager - Manage Docker Registry images remotely
|
||||
# Version: 0.2.1
|
||||
# Version: 0.3.0
|
||||
# Dependencies: curl, jq, bash 4+
|
||||
# Usage: hubmanager --help
|
||||
|
||||
@@ -9,10 +9,12 @@ set -euo pipefail
|
||||
# =============================================================================
|
||||
# --- Constants ---
|
||||
# =============================================================================
|
||||
readonly HM_VERSION="0.2.1"
|
||||
readonly HM_VERSION="0.3.0"
|
||||
readonly HM_DEFAULT_REGISTRY="https://registry-1.docker.io"
|
||||
readonly HM_DEFAULT_CONFIG="${HOME}/.config/hubmanager.conf"
|
||||
readonly HM_DOCKERHUB_API="https://hub.docker.com"
|
||||
readonly HM_KEYRING_KEY="hubmanager:master"
|
||||
readonly HM_KEYRING_DEFAULT_TIMEOUT=300 # seconds (5 minutes)
|
||||
|
||||
# =============================================================================
|
||||
# --- Global state ---
|
||||
@@ -35,6 +37,7 @@ declare -A HM_CONFIG_VARS=() # raw config key/value pairs
|
||||
|
||||
HM_HUB_TOKEN="" # Docker Hub REST API token (JWT)
|
||||
HM_MASTER_PASS="" # Master passphrase for encrypted config values (session-scoped)
|
||||
HM_CACHE_TIMEOUT="$HM_KEYRING_DEFAULT_TIMEOUT" # Keyring cache TTL in seconds
|
||||
HM_TMPFILES=()
|
||||
HM_LAST_HTTP_CODE=""
|
||||
HM_LAST_HEADERS_FILE=""
|
||||
@@ -111,14 +114,54 @@ _require_openssl() {
|
||||
die "openssl is required for encrypted config values. Install openssl and retry."
|
||||
}
|
||||
|
||||
# Prompt once per session; result cached in HM_MASTER_PASS
|
||||
# --- Keyring helpers (optional; keyctl from keyutils) ---
|
||||
|
||||
# Check if keyctl is available
|
||||
_keyring_available() { command -v keyctl &>/dev/null; }
|
||||
|
||||
# Try to read the cached passphrase from the user session keyring.
|
||||
# Outputs the passphrase on stdout; returns 1 if not found or expired.
|
||||
_keyring_get() {
|
||||
_keyring_available || return 1
|
||||
local key_id
|
||||
key_id=$(keyctl request user "$HM_KEYRING_KEY" 2>/dev/null) || return 1
|
||||
keyctl pipe "$key_id" 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
# Store passphrase in the user session keyring with a TTL.
|
||||
# Args: <passphrase> [timeout_seconds]
|
||||
_keyring_set() {
|
||||
_keyring_available || return 0
|
||||
local pass="$1"
|
||||
local ttl="${2:-$HM_CACHE_TIMEOUT}"
|
||||
local key_id
|
||||
key_id=$(keyctl add user "$HM_KEYRING_KEY" "$pass" @u 2>/dev/null) || return 0
|
||||
keyctl timeout "$key_id" "$ttl" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Revoke (clear) the cached passphrase from the keyring.
|
||||
_keyring_clear() {
|
||||
_keyring_available || return 0
|
||||
local key_id
|
||||
key_id=$(keyctl request user "$HM_KEYRING_KEY" 2>/dev/null) || return 0
|
||||
keyctl revoke "$key_id" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Prompt once per session; result cached in HM_MASTER_PASS and keyring
|
||||
_prompt_master_pass() {
|
||||
[[ -n "$HM_MASTER_PASS" ]] && return 0
|
||||
# Try keyring cache first
|
||||
local cached
|
||||
if cached=$(_keyring_get); then
|
||||
HM_MASTER_PASS="$cached"
|
||||
return 0
|
||||
fi
|
||||
_require_openssl
|
||||
printf "hubmanager master passphrase: " >/dev/tty
|
||||
read -rs HM_MASTER_PASS </dev/tty
|
||||
printf "\n" >/dev/tty
|
||||
[[ -n "$HM_MASTER_PASS" ]] || die "Master passphrase cannot be empty."
|
||||
_keyring_set "$HM_MASTER_PASS" "$HM_CACHE_TIMEOUT"
|
||||
}
|
||||
|
||||
# Prompt and confirm a new master passphrase (used by login --encrypt)
|
||||
@@ -132,6 +175,7 @@ _prompt_set_master_pass() {
|
||||
[[ "$pass1" == "$pass2" ]] || die "Passphrases do not match."
|
||||
[[ -n "$pass1" ]] || die "Master passphrase cannot be empty."
|
||||
HM_MASTER_PASS="$pass1"
|
||||
_keyring_set "$HM_MASTER_PASS" "$HM_CACHE_TIMEOUT"
|
||||
}
|
||||
|
||||
# Encrypt PLAINTEXT using AES-256-CBC; outputs base64 ciphertext (no newlines)
|
||||
@@ -208,6 +252,10 @@ load_config() {
|
||||
HM_PASSWORD="$pw"
|
||||
fi
|
||||
fi
|
||||
# Cache timeout (only override if not set via CLI)
|
||||
if [[ "$HM_CACHE_TIMEOUT" == "$HM_KEYRING_DEFAULT_TIMEOUT" && -v 'HM_CONFIG_VARS[CACHE_TIMEOUT]' ]]; then
|
||||
HM_CACHE_TIMEOUT="${HM_CONFIG_VARS[CACHE_TIMEOUT]}"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_registry_alias() {
|
||||
@@ -607,6 +655,7 @@ ${C_BOLD}Options:${C_RESET}
|
||||
-u, --user <username> Username
|
||||
-p, --password <pass> Password or token
|
||||
--config <file> Config file (default: ~/.config/hubmanager.conf)
|
||||
--cache-timeout <s> Passphrase cache TTL in seconds (default: 300)
|
||||
--json Output raw JSON
|
||||
--no-color Disable color output
|
||||
-v, --verbose Verbose mode
|
||||
@@ -622,6 +671,8 @@ ${C_BOLD}Commands:${C_RESET}
|
||||
copy <src> <dst> Copy/retag an image
|
||||
prune <image> Delete outdated tags for an image
|
||||
login Test credentials and optionally save to config (--save [--encrypt])
|
||||
unlock Cache master passphrase in kernel keyring (requires keyctl)
|
||||
lock Clear cached master passphrase
|
||||
|
||||
Run ${C_BOLD}hubmanager <command> --help${C_RESET} for command-specific options.
|
||||
EOF
|
||||
@@ -634,6 +685,50 @@ cmd_help() { show_usage; }
|
||||
# =============================================================================
|
||||
cmd_version() { echo "hubmanager $HM_VERSION"; }
|
||||
|
||||
# =============================================================================
|
||||
# --- Subcommand: unlock ---
|
||||
# =============================================================================
|
||||
cmd_unlock() {
|
||||
if [[ "${1:-}" == "--help" ]]; then
|
||||
cat <<EOF
|
||||
${C_BOLD}Usage:${C_RESET} hubmanager unlock [--timeout <seconds>]
|
||||
|
||||
Cache the master passphrase in the Linux kernel keyring so subsequent
|
||||
commands do not re-prompt. Requires ${C_BOLD}keyctl${C_RESET} (keyutils package).
|
||||
|
||||
--timeout <sec> Cache duration (default: ${HM_KEYRING_DEFAULT_TIMEOUT}s / 5 min)
|
||||
EOF
|
||||
return 0
|
||||
fi
|
||||
_keyring_available || die "keyctl (keyutils) is required for passphrase caching. Install keyutils and retry."
|
||||
_require_openssl
|
||||
# Force a fresh prompt even if already cached
|
||||
HM_MASTER_PASS=""
|
||||
printf "hubmanager master passphrase: " >/dev/tty
|
||||
read -rs HM_MASTER_PASS </dev/tty
|
||||
printf "\n" >/dev/tty
|
||||
[[ -n "$HM_MASTER_PASS" ]] || die "Master passphrase cannot be empty."
|
||||
_keyring_set "$HM_MASTER_PASS" "$HM_CACHE_TIMEOUT"
|
||||
info "Passphrase cached for ${HM_CACHE_TIMEOUT}s."
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# --- Subcommand: lock ---
|
||||
# =============================================================================
|
||||
cmd_lock() {
|
||||
if [[ "${1:-}" == "--help" ]]; then
|
||||
cat <<EOF
|
||||
${C_BOLD}Usage:${C_RESET} hubmanager lock
|
||||
|
||||
Clear the cached master passphrase from the Linux kernel keyring.
|
||||
EOF
|
||||
return 0
|
||||
fi
|
||||
_keyring_clear
|
||||
HM_MASTER_PASS=""
|
||||
info "Passphrase cache cleared."
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# --- Subcommand: login ---
|
||||
# =============================================================================
|
||||
@@ -1594,6 +1689,7 @@ parse_global_args() {
|
||||
-u|--user) HM_USERNAME="$2"; shift 2 ;;
|
||||
-p|--password) HM_PASSWORD="$2"; shift 2 ;;
|
||||
--config) HM_CONFIG_FILE="$2"; shift 2 ;;
|
||||
--cache-timeout) HM_CACHE_TIMEOUT="$2"; shift 2 ;;
|
||||
--json) HM_OPT_JSON=true; shift ;;
|
||||
--no-color) HM_OPT_NO_COLOR=true; shift ;;
|
||||
-v|--verbose) HM_OPT_VERBOSE=true; shift ;;
|
||||
@@ -1635,6 +1731,8 @@ main() {
|
||||
copy) cmd_copy "$@" ;;
|
||||
prune) cmd_prune "$@" ;;
|
||||
login) cmd_login "$@" ;;
|
||||
unlock) cmd_unlock "$@" ;;
|
||||
lock) cmd_lock "$@" ;;
|
||||
help|-h|--help) cmd_help ;;
|
||||
version|--version) cmd_version ;;
|
||||
*)
|
||||
|
||||
Reference in New Issue
Block a user