1 Commits

Author SHA1 Message Date
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
3 changed files with 221 additions and 6 deletions

View File

@@ -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` |

View File

@@ -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) |

View File

@@ -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 ;;
*)