All of lore.kernel.org
 help / color / mirror / Atom feed
* [RESEND][RFC][PATCH 1/2] vcontainer: add --config / VDKR_CONFIG for docker/podman auth credentials
@ 2026-04-29 19:57 tim.orling
  2026-04-29 19:57 ` [RESEND][RFC][PATCH 2/2] tests: add vcontainer --config / VDKR_CONFIG auth plumbing tests tim.orling
  2026-04-29 20:16 ` [meta-virtualization] [RESEND][RFC][PATCH 1/2] vcontainer: add --config / VDKR_CONFIG for docker/podman auth credentials Bruce Ashfield
  0 siblings, 2 replies; 3+ messages in thread
From: tim.orling @ 2026-04-29 19:57 UTC (permalink / raw)
  To: meta-virtualization; +Cc: Tim Orling

From: Tim Orling <tim.orling@konsulko.com>

Add a VDKR_CONFIG / VPDMN_CONFIG env var and a matching --config <path>
CLI flag that passes an existing docker config.json / podman auth.json
into the QEMU-hosted container runtime so pulls from private registries
work without having to retype --registry-user / --registry-pass on every
command.

Security posture (defence in depth):
- Host-side pre-flight validation in vrunner.sh (validate_auth_config):
  reject symlinks, non-regular files, missing / unreadable files, files
  smaller than 2 bytes (minimum "{}") or larger than 1 MiB, and any
  permissions other than 0400 / 0600 / 0200. WARN if not owned by the
  invoking user.
- Stage the file into a dedicated per-invocation directory under
  $TEMP_DIR at mode 0400 inside a 0700 parent; auto-cleanup rides the
  existing EXIT/INT/TERM trap.
- Expose the staged file over a *separate* read-only virtio-9p tag
  ("${TOOL_NAME}_auth") so credentials cannot leak into the general
  /mnt/share input/output directory or into storage.tar outputs.
- Only a boolean flag ("${CMDLINE_PREFIX}_auth=1") is appended to the
  kernel cmdline - never the path, the env var name, or the contents.
- Guest mounts /mnt/auth ro,nosuid,nodev,noexec, copies to the runtime's
  canonical path, then unmounts immediately so neither the runtime nor
  user workloads keep a reference to the host staging directory.

vrunner.sh:
- Initialise AUTH_CONFIG from $VDKR_CONFIG / $VPDMN_CONFIG
- Parse --config <path> (overrides the env vars)
- Add validate_auth_config() and setup_auth_share() with the rules above
- Call setup_auth_share in both the daemon start path and the
  non-daemon / batch-import path

vcontainer-init-common.sh:
- Default RUNTIME_AUTH="0" and parse ${VCONTAINER_RUNTIME_PREFIX}_auth=*
  from the kernel cmdline
- Define mount_auth_share() / unmount_auth_share() using the per-runtime
  "${VCONTAINER_RUNTIME_NAME}_auth" 9p tag, mounted at /mnt/auth with
  ro,nosuid,nodev,noexec

vdkr-init.sh:
- install_auth_config() copies /mnt/auth/config.json to
  /root/.docker/config.json (mode 0600; parent dir 0700)
- Called after install_registry_ca in main flow so --config takes
  precedence over --registry-user / --registry-pass; logs a NOTE when
  both mechanisms are supplied
- Unmounts /mnt/auth after copy

vpdmn-init.sh:
- install_auth_config() copies to /run/containers/0/auth.json (the
  rootful podman canonical path) and exports REGISTRY_AUTH_FILE so the
  creds are picked up regardless of podman's search order
- Mode 0600 on the file, 0700 on the containing directory
- Unmounts /mnt/auth after copy

vcontainer-common.sh:
- Honour $VDKR_CONFIG / $VPDMN_CONFIG, parse --config, and forward
  AUTH_CONFIG to vrunner.sh via --config in build_runner_args
- Document the flag and env vars in show_usage

README.md:
- New "Passing an existing docker/podman auth file (--config)" section
  with examples for both runtimes, a table of target paths, and the
  full security model

AI-Generated: Claude Cowork Opus 4.7
Signed-off-by: Tim Orling <tim.orling@konsulko.com>
---
 recipes-containers/vcontainer/README.md       |  54 +++++++
 .../vcontainer/files/vcontainer-common.sh     |  18 +++
 .../files/vcontainer-init-common.sh           |  54 +++++++
 .../vcontainer/files/vdkr-init.sh             |  58 +++++++
 .../vcontainer/files/vpdmn-init.sh            |  61 ++++++++
 .../vcontainer/files/vrunner.sh               | 147 ++++++++++++++++++
 6 files changed, 392 insertions(+)

diff --git a/recipes-containers/vcontainer/README.md b/recipes-containers/vcontainer/README.md
index 657dd02e..e44616f4 100644
--- a/recipes-containers/vcontainer/README.md
+++ b/recipes-containers/vcontainer/README.md
@@ -317,6 +317,60 @@ use `--secure-registry --ca-cert`:
 vdkr --secure-registry --ca-cert /path/to/ca.crt pull myimage
 ```
 
+### Passing an existing docker/podman auth file (`--config`)
+
+If you already have credentials set up on the host (for example, from
+running `docker login` locally), you can pass the resulting auth file
+straight through into the emulated environment instead of re-entering
+credentials with `--registry-user`/`--registry-pass`:
+
+```bash
+# Docker (vdkr): uses ~/.docker/config.json by default
+vdkr --config ~/.docker/config.json pull registry.example.com/myimage
+
+# Podman (vpdmn): uses $XDG_RUNTIME_DIR/containers/auth.json
+vpdmn --config $XDG_RUNTIME_DIR/containers/auth.json pull registry.example.com/myimage
+```
+
+The path can also be supplied via environment:
+
+```bash
+export VDKR_CONFIG=$HOME/.docker/config.json
+vdkr pull registry.example.com/myimage
+```
+
+(`VPDMN_CONFIG` is honoured identically by `vpdmn`.)
+
+**What the file ends up as inside the VM:**
+
+| Tool  | Target path                       | Notes                                             |
+| ----- | --------------------------------- | ------------------------------------------------- |
+| vdkr  | `/root/.docker/config.json`       | Mode 0600; containing dir 0700                    |
+| vpdmn | `/run/containers/0/auth.json`     | Mode 0600; `$REGISTRY_AUTH_FILE` exported         |
+
+**Security model.** The credential file is treated as secret material:
+
+- The host-side file **must** be a regular file with mode `0600` or `0400`.
+  World/group-readable files are rejected outright. Symlinks are rejected.
+  Files larger than 1 MiB are rejected.
+- On the host it is copied into a per-invocation private directory under
+  `$TMPDIR/vdkr-$$/auth_share` (mode 0700; file mode 0400) and removed
+  automatically by the `EXIT`/`INT`/`TERM` trap when `vrunner.sh` exits.
+- It is exposed to the guest on a **dedicated** virtio-9p share whose
+  mount tag (`vdkr_auth` / `vpdmn_auth`) is distinct from the general
+  `*_share` share used for input/output. The guest mounts it **read-only**
+  at `/mnt/auth`, copies it into the runtime's credential location, then
+  **unmounts** `/mnt/auth` so nothing in the VM retains an open reference
+  to the host staging directory.
+- Nothing about the file appears on the kernel command line. Only a
+  boolean flag (`docker_auth=1` / `podman_auth=1`) is passed so the guest
+  init script knows to look on the auth share.
+- When both `--config` and `--registry-user`/`--registry-pass` are
+  supplied, `--config` wins and a NOTE is logged.
+- `--config` is NOT forwarded into container workloads (it only reaches
+  the container engine's credential store); containers themselves never
+  see `/mnt/auth`.
+
 ## Volume Mounts
 
 Mount host directories into containers using `-v` (requires memory resident mode):
diff --git a/recipes-containers/vcontainer/files/vcontainer-common.sh b/recipes-containers/vcontainer/files/vcontainer-common.sh
index 126ca727..8adec77d 100755
--- a/recipes-containers/vcontainer/files/vcontainer-common.sh
+++ b/recipes-containers/vcontainer/files/vcontainer-common.sh
@@ -718,6 +718,11 @@ ${BOLD}GLOBAL OPTIONS:${NC}
     --registry <url>      Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto)
     --no-registry         Disable baked-in default registry (use images as-is)
     --insecure-registry <host:port>  Mark registry as insecure (HTTP). Can repeat.
+    --config <path>       Registry auth file (docker config.json / podman auth.json)
+                          Defaults to \$VDKR_CONFIG / \$VPDMN_CONFIG. The file must be
+                          mode 0600 or stricter; it is passed to the guest over a
+                          dedicated read-only virtio-9p share and never appears on
+                          the kernel cmdline.
     --verbose, -v         Enable verbose output
     --help, -h            Show this help
 
@@ -857,6 +862,7 @@ build_runner_args() {
     [ -n "$CA_CERT" ] && args+=("--ca-cert" "$CA_CERT")
     [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER")
     [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS")
+    [ -n "$AUTH_CONFIG" ] && args+=("--config" "$AUTH_CONFIG")
 
     # Xen: pass exit grace period
     [ -n "${VXN_EXIT_GRACE_PERIOD:-}" ] && args+=("--exit-grace-period" "$VXN_EXIT_GRACE_PERIOD")
@@ -880,6 +886,11 @@ SECURE_REGISTRY="false"
 CA_CERT=""
 REGISTRY_USER=""
 REGISTRY_PASS=""
+# Registry auth config file. Env-var default depends on which CLI wrapper is
+# in use (vdkr → $VDKR_CONFIG, vpdmn → $VPDMN_CONFIG), then falls back to the
+# other for convenience when sharing a single host-side file. Overridden by
+# the --config CLI flag below.
+AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}"
 COMMAND=""
 COMMAND_ARGS=()
 
@@ -977,6 +988,13 @@ while [ $# -gt 0 ]; do
             REGISTRY_PASS="$2"
             shift 2
             ;;
+        --config)
+            # Path to a docker/podman registry auth file (config.json / auth.json).
+            # Overrides $VDKR_CONFIG / $VPDMN_CONFIG. Forwarded to vrunner.sh --config,
+            # which validates the file and stages it on a dedicated read-only 9p share.
+            AUTH_CONFIG="$2"
+            shift 2
+            ;;
         -it|--interactive)
             INTERACTIVE="true"
             shift
diff --git a/recipes-containers/vcontainer/files/vcontainer-init-common.sh b/recipes-containers/vcontainer/files/vcontainer-init-common.sh
index ab8762b2..3bd70e75 100755
--- a/recipes-containers/vcontainer/files/vcontainer-init-common.sh
+++ b/recipes-containers/vcontainer/files/vcontainer-init-common.sh
@@ -156,6 +156,7 @@ parse_cmdline() {
     RUNTIME_INTERACTIVE="0"
     RUNTIME_DAEMON="0"
     RUNTIME_9P="0"  # virtio-9p available for fast I/O
+    RUNTIME_AUTH="0"  # registry auth config (config.json / auth.json) available on dedicated 9p share
     RUNTIME_IDLE_TIMEOUT="1800"  # Default: 30 minutes
 
     for param in $(cat /proc/cmdline); do
@@ -187,6 +188,9 @@ parse_cmdline() {
             ${VCONTAINER_RUNTIME_PREFIX}_9p=*)
                 RUNTIME_9P="${param#${VCONTAINER_RUNTIME_PREFIX}_9p=}"
                 ;;
+            ${VCONTAINER_RUNTIME_PREFIX}_auth=*)
+                RUNTIME_AUTH="${param#${VCONTAINER_RUNTIME_PREFIX}_auth=}"
+                ;;
         esac
     done
 
@@ -262,6 +266,56 @@ mount_input_disk() {
     fi
 }
 
+# ============================================================================
+# Registry auth share (docker config.json / podman auth.json)
+# ============================================================================
+# The host stages a validated credential file on a *dedicated* read-only 9p
+# share tagged "${VCONTAINER_RUNTIME_NAME}_auth" (e.g. "vdkr_auth" or
+# "vpdmn_auth"). That tag is separate from the general ${VCONTAINER_SHARE_NAME}
+# used for input/output so credentials can't leak into storage.tar outputs or
+# be overwritten by daemon_send_with_input.
+#
+# We mount read-only, nosuid, nodev, noexec at /mnt/auth. Callers are expected
+# to copy the credential file into the runtime's canonical location with
+# restrictive permissions and then call unmount_auth_share() so the guest
+# filesystem no longer has an open reference to the host-side file.
+
+AUTH_SHARE_TAG=""
+AUTH_SHARE_MOUNT="/mnt/auth"
+
+mount_auth_share() {
+    if [ "$RUNTIME_AUTH" != "1" ]; then
+        return 1
+    fi
+
+    AUTH_SHARE_TAG="${VCONTAINER_RUNTIME_NAME}_auth"
+    mkdir -p "$AUTH_SHARE_MOUNT"
+
+    # trans/version/cache match the existing 9p share mount. Add:
+    #   ro      - guest can't mutate the host-side staging directory
+    #   nosuid  - no setuid binaries can be executed from the share
+    #   nodev   - no device nodes honoured even if crafted
+    #   noexec  - no code can execute from the share (auth.json is pure data)
+    if mount -t 9p \
+        -o trans=${NINE_P_TRANSPORT},version=9p2000.L,cache=none,ro,nosuid,nodev,noexec \
+        "$AUTH_SHARE_TAG" "$AUTH_SHARE_MOUNT" 2>/dev/null; then
+        log "Mounted auth 9p share at $AUTH_SHARE_MOUNT (tag: $AUTH_SHARE_TAG, ro)"
+        return 0
+    fi
+
+    log "WARNING: Could not mount auth 9p share ($AUTH_SHARE_TAG)"
+    RUNTIME_AUTH="0"
+    return 1
+}
+
+unmount_auth_share() {
+    if mountpoint -q "$AUTH_SHARE_MOUNT" 2>/dev/null; then
+        umount "$AUTH_SHARE_MOUNT" 2>/dev/null || \
+            umount -l "$AUTH_SHARE_MOUNT" 2>/dev/null || true
+    fi
+    rmdir "$AUTH_SHARE_MOUNT" 2>/dev/null || true
+}
+
 # ============================================================================
 # Network Configuration
 # ============================================================================
diff --git a/recipes-containers/vcontainer/files/vdkr-init.sh b/recipes-containers/vcontainer/files/vdkr-init.sh
index e1e869b2..4ad50668 100755
--- a/recipes-containers/vcontainer/files/vdkr-init.sh
+++ b/recipes-containers/vcontainer/files/vdkr-init.sh
@@ -26,6 +26,10 @@
 #   docker_registry_ca=1                  CA certificate available in /mnt/share/ca.crt
 #   docker_registry_user=<user>           Registry username for authentication
 #   docker_registry_pass=<base64>         Base64-encoded registry password
+#   docker_auth=1                         A pre-built docker config.json is available
+#                                         on a dedicated read-only 9p share tagged
+#                                         "vdkr_auth" (mounted at /mnt/auth). Takes
+#                                         precedence over docker_registry_user/pass.
 #
 # Version: 2.5.0
 
@@ -159,6 +163,55 @@ EOF
     fi
 }
 
+# Install a user-supplied docker config.json from the dedicated read-only
+# auth 9p share (mounted at /mnt/auth by mount_auth_share). This takes
+# precedence over credentials supplied via docker_registry_user/pass.
+#
+# Security posture:
+#   * File is read from a read-only 9p share with a separate tag ("vdkr_auth")
+#     so it cannot leak into /mnt/share outputs.
+#   * Target is written with mode 0600 and the parent dir with mode 0700.
+#   * We unmount /mnt/auth immediately after copying so neither the dockerd
+#     runtime nor user workloads in the VM have an open reference to the
+#     host-side staging directory.
+install_auth_config() {
+    if [ "$RUNTIME_AUTH" != "1" ]; then
+        return 0
+    fi
+
+    if ! mount_auth_share; then
+        log "WARNING: docker_auth=1 was set but the auth 9p share did not mount"
+        return 1
+    fi
+
+    local src="$AUTH_SHARE_MOUNT/config.json"
+    if [ ! -f "$src" ]; then
+        log "WARNING: expected $src on auth share but file is missing"
+        unmount_auth_share
+        return 1
+    fi
+
+    mkdir -p /root/.docker
+    chmod 700 /root/.docker
+
+    if cp "$src" /root/.docker/config.json 2>/dev/null; then
+        chmod 600 /root/.docker/config.json
+        log "Installed registry auth config at /root/.docker/config.json"
+        if [ -n "$DOCKER_REGISTRY_USER" ] || [ -n "$DOCKER_REGISTRY_PASS" ]; then
+            log "NOTE: --config takes precedence over --registry-user/--registry-pass"
+        fi
+    else
+        log "ERROR: failed to copy auth config to /root/.docker/config.json"
+        unmount_auth_share
+        return 1
+    fi
+
+    # Release the host-side share so credentials aren't still addressable
+    # through /mnt/auth for the lifetime of the VM.
+    unmount_auth_share
+    return 0
+}
+
 # ============================================================================
 # Docker-Specific Functions
 # ============================================================================
@@ -684,6 +737,11 @@ parse_secure_registry_config
 # Install CA certificate for secure registry
 install_registry_ca
 
+# Install user-supplied docker config.json from the dedicated auth 9p share.
+# Must run AFTER install_registry_ca so that --config takes precedence when
+# both mechanisms are used.
+install_auth_config
+
 # Start containerd and dockerd (Docker-specific)
 start_containerd
 start_dockerd
diff --git a/recipes-containers/vcontainer/files/vpdmn-init.sh b/recipes-containers/vcontainer/files/vpdmn-init.sh
index 7f661102..2036ed39 100755
--- a/recipes-containers/vcontainer/files/vpdmn-init.sh
+++ b/recipes-containers/vcontainer/files/vpdmn-init.sh
@@ -20,6 +20,12 @@
 #   podman_output=<type>   Output type: text, tar, storage (default: text)
 #   podman_state=<type>    State type: none, disk (default: none)
 #   podman_network=1       Enable networking (configure eth0, DNS)
+#   podman_auth=1          A pre-built registry auth file (docker config.json
+#                          schema, "auths" block) is available on a dedicated
+#                          read-only 9p share tagged "vpdmn_auth" (mounted at
+#                          /mnt/auth). Installed as /run/containers/0/auth.json
+#                          (the rootful podman default), and exported via
+#                          $REGISTRY_AUTH_FILE.
 #
 # Version: 1.1.0
 #
@@ -97,6 +103,57 @@ verify_podman() {
     fi
 }
 
+# Install a user-supplied registry auth file from the dedicated read-only
+# auth 9p share (mounted at /mnt/auth by mount_auth_share). Podman accepts
+# the same "auths" JSON schema as docker config.json, so we can copy directly.
+#
+# Canonical rootful path is /run/containers/0/auth.json; we also export
+# $REGISTRY_AUTH_FILE so it works regardless of podman's search order.
+#
+# Security posture matches vdkr-init.sh install_auth_config:
+#   * Source is a separate read-only 9p tag ("vpdmn_auth") so it cannot leak
+#     into /mnt/share outputs.
+#   * Target has mode 0600; containing dir has mode 0700.
+#   * /mnt/auth is unmounted immediately after copy so user workloads in the
+#     VM have no open reference to the host-side staging directory.
+install_auth_config() {
+    if [ "$RUNTIME_AUTH" != "1" ]; then
+        return 0
+    fi
+
+    if ! mount_auth_share; then
+        log "WARNING: podman_auth=1 was set but the auth 9p share did not mount"
+        return 1
+    fi
+
+    local src="$AUTH_SHARE_MOUNT/config.json"
+    if [ ! -f "$src" ]; then
+        log "WARNING: expected $src on auth share but file is missing"
+        unmount_auth_share
+        return 1
+    fi
+
+    # Rootful podman's default auth path
+    local auth_dir="/run/containers/0"
+    local auth_file="$auth_dir/auth.json"
+
+    mkdir -p "$auth_dir"
+    chmod 700 "$auth_dir"
+
+    if cp "$src" "$auth_file" 2>/dev/null; then
+        chmod 600 "$auth_file"
+        export REGISTRY_AUTH_FILE="$auth_file"
+        log "Installed registry auth config at $auth_file"
+    else
+        log "ERROR: failed to copy auth config to $auth_file"
+        unmount_auth_share
+        return 1
+    fi
+
+    unmount_auth_share
+    return 0
+}
+
 # Podman is daemonless - nothing to stop
 stop_runtime_daemons() {
     :
@@ -190,6 +247,10 @@ configure_networking
 # Verify podman is available (no daemon to start)
 verify_podman
 
+# Install user-supplied auth config from the dedicated auth 9p share, if any.
+# Done before command execution so pulls/logins have credentials available.
+install_auth_config
+
 # Handle daemon mode or single command execution
 if [ "$RUNTIME_DAEMON" = "1" ]; then
     run_daemon_mode
diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh
index 1744245a..2fd61655 100755
--- a/recipes-containers/vcontainer/files/vrunner.sh
+++ b/recipes-containers/vcontainer/files/vrunner.sh
@@ -38,6 +38,13 @@ TARGET_ARCH="${VDKR_ARCH:-${VPDMN_ARCH:-aarch64}}"
 TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}"
 VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}"
 
+# Registry authentication config file (docker config.json / podman auth.json).
+# Can be set via $VDKR_CONFIG or $VPDMN_CONFIG in the environment, and is
+# overridden by the --config CLI flag below. The file is passed into the guest
+# over a dedicated read-only virtio-9p share and installed into the guest
+# container runtime's credential location by the init script.
+AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}"
+
 # Runtime-specific settings (set after parsing --runtime)
 set_runtime_config() {
     case "$RUNTIME" in
@@ -232,6 +239,12 @@ OPTIONS:
     --network, -n        Enable networking (slirp user-mode, outbound only)
     --registry <url>     Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto)
     --insecure-registry <host:port>  Mark registry as insecure (HTTP). Can repeat.
+    --config <path>      Path to docker/podman auth config (config.json / auth.json).
+                         Defaults to $VDKR_CONFIG or $VPDMN_CONFIG from environment.
+                         The file is passed to the guest over a dedicated read-only
+                         virtio-9p share and installed at /root/.docker/config.json
+                         (vdkr) or /run/containers/0/auth.json (vpdmn). The host file
+                         must be a regular file with mode 0600 or stricter.
     --interactive, -it   Run in interactive mode (connects terminal to container)
     --timeout <secs>     QEMU timeout [default: 300]
     --idle-timeout <s>   Daemon idle timeout in seconds [default: 1800]
@@ -406,6 +419,13 @@ while [ $# -gt 0 ]; do
             REGISTRY_PASS="$2"
             shift 2
             ;;
+        --config)
+            # Path to a docker/podman config file (config.json / auth.json)
+            # Overrides $VDKR_CONFIG / $VPDMN_CONFIG. The file is mounted into
+            # the guest via a dedicated read-only virtio-9p share.
+            AUTH_CONFIG="$2"
+            shift 2
+            ;;
         --interactive|-it)
             INTERACTIVE="true"
             shift
@@ -847,6 +867,124 @@ fi
 TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$"
 mkdir -p "$TEMP_DIR"
 
+# ============================================================================
+# Registry auth config (docker config.json / podman auth.json)
+# ============================================================================
+# The AUTH_CONFIG path (from $VDKR_CONFIG, $VPDMN_CONFIG, or --config) points
+# to a file containing container-registry credentials. For defence-in-depth we:
+#   * reject non-regular files (symlinks, devices, directories)
+#   * reject files readable by group/other (mode must be <= 0600)
+#   * warn if the file is not owned by the invoking user
+#   * copy it into a private per-invocation directory under $TEMP_DIR at 0400
+#   * expose it to the guest via a *separate* read-only virtio-9p tag
+#     ("${TOOL_NAME}_auth") mounted at /mnt/auth (not the generic /mnt/share
+#     which holds input/output and is wiped between daemon commands)
+#   * never pass the file contents or path on the kernel cmdline; only a flag
+#     "${CMDLINE_PREFIX}_auth=1" to tell the init script to look at /mnt/auth
+#   * rely on the existing $TEMP_DIR EXIT/INT/TERM trap to delete the copy
+#
+# The auth file is never logged (path is visible, but contents are not).
+AUTH_SHARE_DIR=""
+
+validate_auth_config() {
+    local path="$1"
+
+    # Resolve symlinks to the canonical path so the perm check applies to the
+    # actual file, but still require the *named* path to be a regular file
+    # (not a symlink pointing into sensitive areas like /proc/self/environ).
+    if [ -L "$path" ]; then
+        log "ERROR" "--config must not be a symlink: $path"
+        return 1
+    fi
+    if [ ! -e "$path" ]; then
+        log "ERROR" "--config file not found: $path"
+        return 1
+    fi
+    if [ ! -f "$path" ]; then
+        log "ERROR" "--config must be a regular file: $path"
+        return 1
+    fi
+    if [ ! -r "$path" ]; then
+        log "ERROR" "--config file is not readable: $path"
+        return 1
+    fi
+
+    # Size sanity: docker config.json / podman auth.json should be small.
+    # 1 MiB is already generous. Reject unusually large files to avoid
+    # accidentally shipping a large credential blob.
+    local size
+    size=$(stat -c %s "$path" 2>/dev/null || echo 0)
+    if [ "$size" -gt 1048576 ]; then
+        log "ERROR" "--config file is too large ($size bytes, max 1 MiB): $path"
+        return 1
+    fi
+    # Minimum valid JSON object "{}" is 2 bytes. Anything smaller (including a
+    # 0-byte truncation or a lone newline from "echo '' > file") can't be a
+    # real auth config; reject rather than silently shipping garbage.
+    if [ "$size" -lt 2 ]; then
+        log "ERROR" "--config file is empty or too small to be valid JSON: $path"
+        return 1
+    fi
+
+    # Permission check: must not be readable by group or world.
+    local mode
+    mode=$(stat -c %a "$path" 2>/dev/null || echo 0)
+    # stat %a emits octal without leading zero. Forbid any group/other bits.
+    case "$mode" in
+        400|600|200) ;;
+        *)
+            log "ERROR" "--config file has unsafe permissions ($mode); expected 0600 or 0400."
+            log "ERROR" "Fix with: chmod 600 \"$path\""
+            return 1
+            ;;
+    esac
+
+    # Ownership check: warn if file is not owned by the current user.
+    local uid owner
+    uid=$(id -u)
+    owner=$(stat -c %u "$path" 2>/dev/null || echo "")
+    if [ -n "$owner" ] && [ "$owner" != "$uid" ]; then
+        log "WARN" "--config file is not owned by current user (uid=$uid, owner=$owner)"
+    fi
+
+    return 0
+}
+
+# Stage the auth config into a dedicated read-only 9p share. Must be called
+# AFTER $TEMP_DIR exists and AFTER hypervisor backend functions are sourced.
+# Sets $AUTH_SHARE_DIR and appends to $HV_OPTS / $KERNEL_APPEND.
+setup_auth_share() {
+    [ -z "$AUTH_CONFIG" ] && return 0
+
+    if ! validate_auth_config "$AUTH_CONFIG"; then
+        log "ERROR" "Refusing to stage $AUTH_CONFIG — see above."
+        exit 1
+    fi
+
+    AUTH_SHARE_DIR="$TEMP_DIR/auth_share"
+    # 0700 so nothing outside our process can peek at the staged file.
+    mkdir -p "$AUTH_SHARE_DIR"
+    chmod 700 "$AUTH_SHARE_DIR"
+
+    # Always stage as config.json regardless of source filename — the guest
+    # init script knows to look for this fixed name.
+    if ! cp "$AUTH_CONFIG" "$AUTH_SHARE_DIR/config.json"; then
+        log "ERROR" "Failed to stage auth config"
+        exit 1
+    fi
+    chmod 400 "$AUTH_SHARE_DIR/config.json"
+
+    local auth_tag="${TOOL_NAME}_auth"
+    hv_build_9p_opts "$AUTH_SHARE_DIR" "$auth_tag" "readonly=on"
+    KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_auth=1"
+
+    # Deliberately log the *fact* of staging, not the path contents or
+    # credentials. The path itself is useful for debugging and appears in
+    # --verbose mode only.
+    log "INFO" "Registry auth config staged on read-only 9p share (tag=$auth_tag)"
+    log "DEBUG" "Auth source: $AUTH_CONFIG"
+}
+
 cleanup() {
     if [ "$KEEP_TEMP" = "true" ]; then
         log "DEBUG" "Keeping temp directory: $TEMP_DIR"
@@ -1310,6 +1448,10 @@ if [ "$DAEMON_MODE" = "start" ]; then
         log "DEBUG" "CA certificate copied to shared folder"
     fi
 
+    # Stage registry auth config (config.json / auth.json) on a dedicated
+    # read-only 9p share. See setup_auth_share() for the security model.
+    setup_auth_share
+
     log "INFO" "Starting daemon..."
     log "DEBUG" "PID file: $DAEMON_PID_FILE"
     log "DEBUG" "Socket: $DAEMON_SOCKET"
@@ -1439,6 +1581,11 @@ if [ -n "$CA_CERT" ] && [ -f "$CA_CERT" ]; then
     log "DEBUG" "CA certificate available via 9p"
 fi
 
+# Stage registry auth config (config.json / auth.json) on a dedicated read-only
+# 9p share for non-daemon and batch-import modes. Safe to call when AUTH_CONFIG
+# is empty — it no-ops. See setup_auth_share() for the security model.
+setup_auth_share
+
 log "INFO" "Starting VM ($VCONTAINER_HYPERVISOR)..."
 
 # Interactive mode runs VM in foreground with stdio connected
-- 
2.47.3



^ permalink raw reply related	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-04-29 20:16 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-29 19:57 [RESEND][RFC][PATCH 1/2] vcontainer: add --config / VDKR_CONFIG for docker/podman auth credentials tim.orling
2026-04-29 19:57 ` [RESEND][RFC][PATCH 2/2] tests: add vcontainer --config / VDKR_CONFIG auth plumbing tests tim.orling
2026-04-29 20:16 ` [meta-virtualization] [RESEND][RFC][PATCH 1/2] vcontainer: add --config / VDKR_CONFIG for docker/podman auth credentials Bruce Ashfield

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.