summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--recipes-containers/vcontainer/README.md54
-rwxr-xr-xrecipes-containers/vcontainer/files/vcontainer-common.sh18
-rwxr-xr-xrecipes-containers/vcontainer/files/vcontainer-init-common.sh54
-rwxr-xr-xrecipes-containers/vcontainer/files/vdkr-init.sh58
-rwxr-xr-xrecipes-containers/vcontainer/files/vpdmn-init.sh61
-rwxr-xr-xrecipes-containers/vcontainer/files/vrunner.sh147
6 files changed, 392 insertions, 0 deletions
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`:
317vdkr --secure-registry --ca-cert /path/to/ca.crt pull myimage 317vdkr --secure-registry --ca-cert /path/to/ca.crt pull myimage
318``` 318```
319 319
320### Passing an existing docker/podman auth file (`--config`)
321
322If you already have credentials set up on the host (for example, from
323running `docker login` locally), you can pass the resulting auth file
324straight through into the emulated environment instead of re-entering
325credentials with `--registry-user`/`--registry-pass`:
326
327```bash
328# Docker (vdkr): uses ~/.docker/config.json by default
329vdkr --config ~/.docker/config.json pull registry.example.com/myimage
330
331# Podman (vpdmn): uses $XDG_RUNTIME_DIR/containers/auth.json
332vpdmn --config $XDG_RUNTIME_DIR/containers/auth.json pull registry.example.com/myimage
333```
334
335The path can also be supplied via environment:
336
337```bash
338export VDKR_CONFIG=$HOME/.docker/config.json
339vdkr pull registry.example.com/myimage
340```
341
342(`VPDMN_CONFIG` is honoured identically by `vpdmn`.)
343
344**What the file ends up as inside the VM:**
345
346| Tool | Target path | Notes |
347| ----- | --------------------------------- | ------------------------------------------------- |
348| vdkr | `/root/.docker/config.json` | Mode 0600; containing dir 0700 |
349| vpdmn | `/run/containers/0/auth.json` | Mode 0600; `$REGISTRY_AUTH_FILE` exported |
350
351**Security model.** The credential file is treated as secret material:
352
353- The host-side file **must** be a regular file with mode `0600` or `0400`.
354 World/group-readable files are rejected outright. Symlinks are rejected.
355 Files larger than 1 MiB are rejected.
356- On the host it is copied into a per-invocation private directory under
357 `$TMPDIR/vdkr-$$/auth_share` (mode 0700; file mode 0400) and removed
358 automatically by the `EXIT`/`INT`/`TERM` trap when `vrunner.sh` exits.
359- It is exposed to the guest on a **dedicated** virtio-9p share whose
360 mount tag (`vdkr_auth` / `vpdmn_auth`) is distinct from the general
361 `*_share` share used for input/output. The guest mounts it **read-only**
362 at `/mnt/auth`, copies it into the runtime's credential location, then
363 **unmounts** `/mnt/auth` so nothing in the VM retains an open reference
364 to the host staging directory.
365- Nothing about the file appears on the kernel command line. Only a
366 boolean flag (`docker_auth=1` / `podman_auth=1`) is passed so the guest
367 init script knows to look on the auth share.
368- When both `--config` and `--registry-user`/`--registry-pass` are
369 supplied, `--config` wins and a NOTE is logged.
370- `--config` is NOT forwarded into container workloads (it only reaches
371 the container engine's credential store); containers themselves never
372 see `/mnt/auth`.
373
320## Volume Mounts 374## Volume Mounts
321 375
322Mount host directories into containers using `-v` (requires memory resident mode): 376Mount 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}
718 --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) 718 --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto)
719 --no-registry Disable baked-in default registry (use images as-is) 719 --no-registry Disable baked-in default registry (use images as-is)
720 --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. 720 --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat.
721 --config <path> Registry auth file (docker config.json / podman auth.json)
722 Defaults to \$VDKR_CONFIG / \$VPDMN_CONFIG. The file must be
723 mode 0600 or stricter; it is passed to the guest over a
724 dedicated read-only virtio-9p share and never appears on
725 the kernel cmdline.
721 --verbose, -v Enable verbose output 726 --verbose, -v Enable verbose output
722 --help, -h Show this help 727 --help, -h Show this help
723 728
@@ -857,6 +862,7 @@ build_runner_args() {
857 [ -n "$CA_CERT" ] && args+=("--ca-cert" "$CA_CERT") 862 [ -n "$CA_CERT" ] && args+=("--ca-cert" "$CA_CERT")
858 [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER") 863 [ -n "$REGISTRY_USER" ] && args+=("--registry-user" "$REGISTRY_USER")
859 [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS") 864 [ -n "$REGISTRY_PASS" ] && args+=("--registry-pass" "$REGISTRY_PASS")
865 [ -n "$AUTH_CONFIG" ] && args+=("--config" "$AUTH_CONFIG")
860 866
861 # Xen: pass exit grace period 867 # Xen: pass exit grace period
862 [ -n "${VXN_EXIT_GRACE_PERIOD:-}" ] && args+=("--exit-grace-period" "$VXN_EXIT_GRACE_PERIOD") 868 [ -n "${VXN_EXIT_GRACE_PERIOD:-}" ] && args+=("--exit-grace-period" "$VXN_EXIT_GRACE_PERIOD")
@@ -880,6 +886,11 @@ SECURE_REGISTRY="false"
880CA_CERT="" 886CA_CERT=""
881REGISTRY_USER="" 887REGISTRY_USER=""
882REGISTRY_PASS="" 888REGISTRY_PASS=""
889# Registry auth config file. Env-var default depends on which CLI wrapper is
890# in use (vdkr → $VDKR_CONFIG, vpdmn → $VPDMN_CONFIG), then falls back to the
891# other for convenience when sharing a single host-side file. Overridden by
892# the --config CLI flag below.
893AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}"
883COMMAND="" 894COMMAND=""
884COMMAND_ARGS=() 895COMMAND_ARGS=()
885 896
@@ -977,6 +988,13 @@ while [ $# -gt 0 ]; do
977 REGISTRY_PASS="$2" 988 REGISTRY_PASS="$2"
978 shift 2 989 shift 2
979 ;; 990 ;;
991 --config)
992 # Path to a docker/podman registry auth file (config.json / auth.json).
993 # Overrides $VDKR_CONFIG / $VPDMN_CONFIG. Forwarded to vrunner.sh --config,
994 # which validates the file and stages it on a dedicated read-only 9p share.
995 AUTH_CONFIG="$2"
996 shift 2
997 ;;
980 -it|--interactive) 998 -it|--interactive)
981 INTERACTIVE="true" 999 INTERACTIVE="true"
982 shift 1000 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() {
156 RUNTIME_INTERACTIVE="0" 156 RUNTIME_INTERACTIVE="0"
157 RUNTIME_DAEMON="0" 157 RUNTIME_DAEMON="0"
158 RUNTIME_9P="0" # virtio-9p available for fast I/O 158 RUNTIME_9P="0" # virtio-9p available for fast I/O
159 RUNTIME_AUTH="0" # registry auth config (config.json / auth.json) available on dedicated 9p share
159 RUNTIME_IDLE_TIMEOUT="1800" # Default: 30 minutes 160 RUNTIME_IDLE_TIMEOUT="1800" # Default: 30 minutes
160 161
161 for param in $(cat /proc/cmdline); do 162 for param in $(cat /proc/cmdline); do
@@ -187,6 +188,9 @@ parse_cmdline() {
187 ${VCONTAINER_RUNTIME_PREFIX}_9p=*) 188 ${VCONTAINER_RUNTIME_PREFIX}_9p=*)
188 RUNTIME_9P="${param#${VCONTAINER_RUNTIME_PREFIX}_9p=}" 189 RUNTIME_9P="${param#${VCONTAINER_RUNTIME_PREFIX}_9p=}"
189 ;; 190 ;;
191 ${VCONTAINER_RUNTIME_PREFIX}_auth=*)
192 RUNTIME_AUTH="${param#${VCONTAINER_RUNTIME_PREFIX}_auth=}"
193 ;;
190 esac 194 esac
191 done 195 done
192 196
@@ -263,6 +267,56 @@ mount_input_disk() {
263} 267}
264 268
265# ============================================================================ 269# ============================================================================
270# Registry auth share (docker config.json / podman auth.json)
271# ============================================================================
272# The host stages a validated credential file on a *dedicated* read-only 9p
273# share tagged "${VCONTAINER_RUNTIME_NAME}_auth" (e.g. "vdkr_auth" or
274# "vpdmn_auth"). That tag is separate from the general ${VCONTAINER_SHARE_NAME}
275# used for input/output so credentials can't leak into storage.tar outputs or
276# be overwritten by daemon_send_with_input.
277#
278# We mount read-only, nosuid, nodev, noexec at /mnt/auth. Callers are expected
279# to copy the credential file into the runtime's canonical location with
280# restrictive permissions and then call unmount_auth_share() so the guest
281# filesystem no longer has an open reference to the host-side file.
282
283AUTH_SHARE_TAG=""
284AUTH_SHARE_MOUNT="/mnt/auth"
285
286mount_auth_share() {
287 if [ "$RUNTIME_AUTH" != "1" ]; then
288 return 1
289 fi
290
291 AUTH_SHARE_TAG="${VCONTAINER_RUNTIME_NAME}_auth"
292 mkdir -p "$AUTH_SHARE_MOUNT"
293
294 # trans/version/cache match the existing 9p share mount. Add:
295 # ro - guest can't mutate the host-side staging directory
296 # nosuid - no setuid binaries can be executed from the share
297 # nodev - no device nodes honoured even if crafted
298 # noexec - no code can execute from the share (auth.json is pure data)
299 if mount -t 9p \
300 -o trans=${NINE_P_TRANSPORT},version=9p2000.L,cache=none,ro,nosuid,nodev,noexec \
301 "$AUTH_SHARE_TAG" "$AUTH_SHARE_MOUNT" 2>/dev/null; then
302 log "Mounted auth 9p share at $AUTH_SHARE_MOUNT (tag: $AUTH_SHARE_TAG, ro)"
303 return 0
304 fi
305
306 log "WARNING: Could not mount auth 9p share ($AUTH_SHARE_TAG)"
307 RUNTIME_AUTH="0"
308 return 1
309}
310
311unmount_auth_share() {
312 if mountpoint -q "$AUTH_SHARE_MOUNT" 2>/dev/null; then
313 umount "$AUTH_SHARE_MOUNT" 2>/dev/null || \
314 umount -l "$AUTH_SHARE_MOUNT" 2>/dev/null || true
315 fi
316 rmdir "$AUTH_SHARE_MOUNT" 2>/dev/null || true
317}
318
319# ============================================================================
266# Network Configuration 320# Network Configuration
267# ============================================================================ 321# ============================================================================
268 322
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 @@
26# docker_registry_ca=1 CA certificate available in /mnt/share/ca.crt 26# docker_registry_ca=1 CA certificate available in /mnt/share/ca.crt
27# docker_registry_user=<user> Registry username for authentication 27# docker_registry_user=<user> Registry username for authentication
28# docker_registry_pass=<base64> Base64-encoded registry password 28# docker_registry_pass=<base64> Base64-encoded registry password
29# docker_auth=1 A pre-built docker config.json is available
30# on a dedicated read-only 9p share tagged
31# "vdkr_auth" (mounted at /mnt/auth). Takes
32# precedence over docker_registry_user/pass.
29# 33#
30# Version: 2.5.0 34# Version: 2.5.0
31 35
@@ -159,6 +163,55 @@ EOF
159 fi 163 fi
160} 164}
161 165
166# Install a user-supplied docker config.json from the dedicated read-only
167# auth 9p share (mounted at /mnt/auth by mount_auth_share). This takes
168# precedence over credentials supplied via docker_registry_user/pass.
169#
170# Security posture:
171# * File is read from a read-only 9p share with a separate tag ("vdkr_auth")
172# so it cannot leak into /mnt/share outputs.
173# * Target is written with mode 0600 and the parent dir with mode 0700.
174# * We unmount /mnt/auth immediately after copying so neither the dockerd
175# runtime nor user workloads in the VM have an open reference to the
176# host-side staging directory.
177install_auth_config() {
178 if [ "$RUNTIME_AUTH" != "1" ]; then
179 return 0
180 fi
181
182 if ! mount_auth_share; then
183 log "WARNING: docker_auth=1 was set but the auth 9p share did not mount"
184 return 1
185 fi
186
187 local src="$AUTH_SHARE_MOUNT/config.json"
188 if [ ! -f "$src" ]; then
189 log "WARNING: expected $src on auth share but file is missing"
190 unmount_auth_share
191 return 1
192 fi
193
194 mkdir -p /root/.docker
195 chmod 700 /root/.docker
196
197 if cp "$src" /root/.docker/config.json 2>/dev/null; then
198 chmod 600 /root/.docker/config.json
199 log "Installed registry auth config at /root/.docker/config.json"
200 if [ -n "$DOCKER_REGISTRY_USER" ] || [ -n "$DOCKER_REGISTRY_PASS" ]; then
201 log "NOTE: --config takes precedence over --registry-user/--registry-pass"
202 fi
203 else
204 log "ERROR: failed to copy auth config to /root/.docker/config.json"
205 unmount_auth_share
206 return 1
207 fi
208
209 # Release the host-side share so credentials aren't still addressable
210 # through /mnt/auth for the lifetime of the VM.
211 unmount_auth_share
212 return 0
213}
214
162# ============================================================================ 215# ============================================================================
163# Docker-Specific Functions 216# Docker-Specific Functions
164# ============================================================================ 217# ============================================================================
@@ -684,6 +737,11 @@ parse_secure_registry_config
684# Install CA certificate for secure registry 737# Install CA certificate for secure registry
685install_registry_ca 738install_registry_ca
686 739
740# Install user-supplied docker config.json from the dedicated auth 9p share.
741# Must run AFTER install_registry_ca so that --config takes precedence when
742# both mechanisms are used.
743install_auth_config
744
687# Start containerd and dockerd (Docker-specific) 745# Start containerd and dockerd (Docker-specific)
688start_containerd 746start_containerd
689start_dockerd 747start_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 @@
20# podman_output=<type> Output type: text, tar, storage (default: text) 20# podman_output=<type> Output type: text, tar, storage (default: text)
21# podman_state=<type> State type: none, disk (default: none) 21# podman_state=<type> State type: none, disk (default: none)
22# podman_network=1 Enable networking (configure eth0, DNS) 22# podman_network=1 Enable networking (configure eth0, DNS)
23# podman_auth=1 A pre-built registry auth file (docker config.json
24# schema, "auths" block) is available on a dedicated
25# read-only 9p share tagged "vpdmn_auth" (mounted at
26# /mnt/auth). Installed as /run/containers/0/auth.json
27# (the rootful podman default), and exported via
28# $REGISTRY_AUTH_FILE.
23# 29#
24# Version: 1.1.0 30# Version: 1.1.0
25# 31#
@@ -97,6 +103,57 @@ verify_podman() {
97 fi 103 fi
98} 104}
99 105
106# Install a user-supplied registry auth file from the dedicated read-only
107# auth 9p share (mounted at /mnt/auth by mount_auth_share). Podman accepts
108# the same "auths" JSON schema as docker config.json, so we can copy directly.
109#
110# Canonical rootful path is /run/containers/0/auth.json; we also export
111# $REGISTRY_AUTH_FILE so it works regardless of podman's search order.
112#
113# Security posture matches vdkr-init.sh install_auth_config:
114# * Source is a separate read-only 9p tag ("vpdmn_auth") so it cannot leak
115# into /mnt/share outputs.
116# * Target has mode 0600; containing dir has mode 0700.
117# * /mnt/auth is unmounted immediately after copy so user workloads in the
118# VM have no open reference to the host-side staging directory.
119install_auth_config() {
120 if [ "$RUNTIME_AUTH" != "1" ]; then
121 return 0
122 fi
123
124 if ! mount_auth_share; then
125 log "WARNING: podman_auth=1 was set but the auth 9p share did not mount"
126 return 1
127 fi
128
129 local src="$AUTH_SHARE_MOUNT/config.json"
130 if [ ! -f "$src" ]; then
131 log "WARNING: expected $src on auth share but file is missing"
132 unmount_auth_share
133 return 1
134 fi
135
136 # Rootful podman's default auth path
137 local auth_dir="/run/containers/0"
138 local auth_file="$auth_dir/auth.json"
139
140 mkdir -p "$auth_dir"
141 chmod 700 "$auth_dir"
142
143 if cp "$src" "$auth_file" 2>/dev/null; then
144 chmod 600 "$auth_file"
145 export REGISTRY_AUTH_FILE="$auth_file"
146 log "Installed registry auth config at $auth_file"
147 else
148 log "ERROR: failed to copy auth config to $auth_file"
149 unmount_auth_share
150 return 1
151 fi
152
153 unmount_auth_share
154 return 0
155}
156
100# Podman is daemonless - nothing to stop 157# Podman is daemonless - nothing to stop
101stop_runtime_daemons() { 158stop_runtime_daemons() {
102 : 159 :
@@ -190,6 +247,10 @@ configure_networking
190# Verify podman is available (no daemon to start) 247# Verify podman is available (no daemon to start)
191verify_podman 248verify_podman
192 249
250# Install user-supplied auth config from the dedicated auth 9p share, if any.
251# Done before command execution so pulls/logins have credentials available.
252install_auth_config
253
193# Handle daemon mode or single command execution 254# Handle daemon mode or single command execution
194if [ "$RUNTIME_DAEMON" = "1" ]; then 255if [ "$RUNTIME_DAEMON" = "1" ]; then
195 run_daemon_mode 256 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}}"
38TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}" 38TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}"
39VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}" 39VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}"
40 40
41# Registry authentication config file (docker config.json / podman auth.json).
42# Can be set via $VDKR_CONFIG or $VPDMN_CONFIG in the environment, and is
43# overridden by the --config CLI flag below. The file is passed into the guest
44# over a dedicated read-only virtio-9p share and installed into the guest
45# container runtime's credential location by the init script.
46AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}"
47
41# Runtime-specific settings (set after parsing --runtime) 48# Runtime-specific settings (set after parsing --runtime)
42set_runtime_config() { 49set_runtime_config() {
43 case "$RUNTIME" in 50 case "$RUNTIME" in
@@ -232,6 +239,12 @@ OPTIONS:
232 --network, -n Enable networking (slirp user-mode, outbound only) 239 --network, -n Enable networking (slirp user-mode, outbound only)
233 --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto) 240 --registry <url> Default registry for unqualified images (e.g., 10.0.2.2:5000/yocto)
234 --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat. 241 --insecure-registry <host:port> Mark registry as insecure (HTTP). Can repeat.
242 --config <path> Path to docker/podman auth config (config.json / auth.json).
243 Defaults to $VDKR_CONFIG or $VPDMN_CONFIG from environment.
244 The file is passed to the guest over a dedicated read-only
245 virtio-9p share and installed at /root/.docker/config.json
246 (vdkr) or /run/containers/0/auth.json (vpdmn). The host file
247 must be a regular file with mode 0600 or stricter.
235 --interactive, -it Run in interactive mode (connects terminal to container) 248 --interactive, -it Run in interactive mode (connects terminal to container)
236 --timeout <secs> QEMU timeout [default: 300] 249 --timeout <secs> QEMU timeout [default: 300]
237 --idle-timeout <s> Daemon idle timeout in seconds [default: 1800] 250 --idle-timeout <s> Daemon idle timeout in seconds [default: 1800]
@@ -406,6 +419,13 @@ while [ $# -gt 0 ]; do
406 REGISTRY_PASS="$2" 419 REGISTRY_PASS="$2"
407 shift 2 420 shift 2
408 ;; 421 ;;
422 --config)
423 # Path to a docker/podman config file (config.json / auth.json)
424 # Overrides $VDKR_CONFIG / $VPDMN_CONFIG. The file is mounted into
425 # the guest via a dedicated read-only virtio-9p share.
426 AUTH_CONFIG="$2"
427 shift 2
428 ;;
409 --interactive|-it) 429 --interactive|-it)
410 INTERACTIVE="true" 430 INTERACTIVE="true"
411 shift 431 shift
@@ -847,6 +867,124 @@ fi
847TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$" 867TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$"
848mkdir -p "$TEMP_DIR" 868mkdir -p "$TEMP_DIR"
849 869
870# ============================================================================
871# Registry auth config (docker config.json / podman auth.json)
872# ============================================================================
873# The AUTH_CONFIG path (from $VDKR_CONFIG, $VPDMN_CONFIG, or --config) points
874# to a file containing container-registry credentials. For defence-in-depth we:
875# * reject non-regular files (symlinks, devices, directories)
876# * reject files readable by group/other (mode must be <= 0600)
877# * warn if the file is not owned by the invoking user
878# * copy it into a private per-invocation directory under $TEMP_DIR at 0400
879# * expose it to the guest via a *separate* read-only virtio-9p tag
880# ("${TOOL_NAME}_auth") mounted at /mnt/auth (not the generic /mnt/share
881# which holds input/output and is wiped between daemon commands)
882# * never pass the file contents or path on the kernel cmdline; only a flag
883# "${CMDLINE_PREFIX}_auth=1" to tell the init script to look at /mnt/auth
884# * rely on the existing $TEMP_DIR EXIT/INT/TERM trap to delete the copy
885#
886# The auth file is never logged (path is visible, but contents are not).
887AUTH_SHARE_DIR=""
888
889validate_auth_config() {
890 local path="$1"
891
892 # Resolve symlinks to the canonical path so the perm check applies to the
893 # actual file, but still require the *named* path to be a regular file
894 # (not a symlink pointing into sensitive areas like /proc/self/environ).
895 if [ -L "$path" ]; then
896 log "ERROR" "--config must not be a symlink: $path"
897 return 1
898 fi
899 if [ ! -e "$path" ]; then
900 log "ERROR" "--config file not found: $path"
901 return 1
902 fi
903 if [ ! -f "$path" ]; then
904 log "ERROR" "--config must be a regular file: $path"
905 return 1
906 fi
907 if [ ! -r "$path" ]; then
908 log "ERROR" "--config file is not readable: $path"
909 return 1
910 fi
911
912 # Size sanity: docker config.json / podman auth.json should be small.
913 # 1 MiB is already generous. Reject unusually large files to avoid
914 # accidentally shipping a large credential blob.
915 local size
916 size=$(stat -c %s "$path" 2>/dev/null || echo 0)
917 if [ "$size" -gt 1048576 ]; then
918 log "ERROR" "--config file is too large ($size bytes, max 1 MiB): $path"
919 return 1
920 fi
921 # Minimum valid JSON object "{}" is 2 bytes. Anything smaller (including a
922 # 0-byte truncation or a lone newline from "echo '' > file") can't be a
923 # real auth config; reject rather than silently shipping garbage.
924 if [ "$size" -lt 2 ]; then
925 log "ERROR" "--config file is empty or too small to be valid JSON: $path"
926 return 1
927 fi
928
929 # Permission check: must not be readable by group or world.
930 local mode
931 mode=$(stat -c %a "$path" 2>/dev/null || echo 0)
932 # stat %a emits octal without leading zero. Forbid any group/other bits.
933 case "$mode" in
934 400|600|200) ;;
935 *)
936 log "ERROR" "--config file has unsafe permissions ($mode); expected 0600 or 0400."
937 log "ERROR" "Fix with: chmod 600 \"$path\""
938 return 1
939 ;;
940 esac
941
942 # Ownership check: warn if file is not owned by the current user.
943 local uid owner
944 uid=$(id -u)
945 owner=$(stat -c %u "$path" 2>/dev/null || echo "")
946 if [ -n "$owner" ] && [ "$owner" != "$uid" ]; then
947 log "WARN" "--config file is not owned by current user (uid=$uid, owner=$owner)"
948 fi
949
950 return 0
951}
952
953# Stage the auth config into a dedicated read-only 9p share. Must be called
954# AFTER $TEMP_DIR exists and AFTER hypervisor backend functions are sourced.
955# Sets $AUTH_SHARE_DIR and appends to $HV_OPTS / $KERNEL_APPEND.
956setup_auth_share() {
957 [ -z "$AUTH_CONFIG" ] && return 0
958
959 if ! validate_auth_config "$AUTH_CONFIG"; then
960 log "ERROR" "Refusing to stage $AUTH_CONFIG — see above."
961 exit 1
962 fi
963
964 AUTH_SHARE_DIR="$TEMP_DIR/auth_share"
965 # 0700 so nothing outside our process can peek at the staged file.
966 mkdir -p "$AUTH_SHARE_DIR"
967 chmod 700 "$AUTH_SHARE_DIR"
968
969 # Always stage as config.json regardless of source filename — the guest
970 # init script knows to look for this fixed name.
971 if ! cp "$AUTH_CONFIG" "$AUTH_SHARE_DIR/config.json"; then
972 log "ERROR" "Failed to stage auth config"
973 exit 1
974 fi
975 chmod 400 "$AUTH_SHARE_DIR/config.json"
976
977 local auth_tag="${TOOL_NAME}_auth"
978 hv_build_9p_opts "$AUTH_SHARE_DIR" "$auth_tag" "readonly=on"
979 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_auth=1"
980
981 # Deliberately log the *fact* of staging, not the path contents or
982 # credentials. The path itself is useful for debugging and appears in
983 # --verbose mode only.
984 log "INFO" "Registry auth config staged on read-only 9p share (tag=$auth_tag)"
985 log "DEBUG" "Auth source: $AUTH_CONFIG"
986}
987
850cleanup() { 988cleanup() {
851 if [ "$KEEP_TEMP" = "true" ]; then 989 if [ "$KEEP_TEMP" = "true" ]; then
852 log "DEBUG" "Keeping temp directory: $TEMP_DIR" 990 log "DEBUG" "Keeping temp directory: $TEMP_DIR"
@@ -1310,6 +1448,10 @@ if [ "$DAEMON_MODE" = "start" ]; then
1310 log "DEBUG" "CA certificate copied to shared folder" 1448 log "DEBUG" "CA certificate copied to shared folder"
1311 fi 1449 fi
1312 1450
1451 # Stage registry auth config (config.json / auth.json) on a dedicated
1452 # read-only 9p share. See setup_auth_share() for the security model.
1453 setup_auth_share
1454
1313 log "INFO" "Starting daemon..." 1455 log "INFO" "Starting daemon..."
1314 log "DEBUG" "PID file: $DAEMON_PID_FILE" 1456 log "DEBUG" "PID file: $DAEMON_PID_FILE"
1315 log "DEBUG" "Socket: $DAEMON_SOCKET" 1457 log "DEBUG" "Socket: $DAEMON_SOCKET"
@@ -1439,6 +1581,11 @@ if [ -n "$CA_CERT" ] && [ -f "$CA_CERT" ]; then
1439 log "DEBUG" "CA certificate available via 9p" 1581 log "DEBUG" "CA certificate available via 9p"
1440fi 1582fi
1441 1583
1584# Stage registry auth config (config.json / auth.json) on a dedicated read-only
1585# 9p share for non-daemon and batch-import modes. Safe to call when AUTH_CONFIG
1586# is empty — it no-ops. See setup_auth_share() for the security model.
1587setup_auth_share
1588
1442log "INFO" "Starting VM ($VCONTAINER_HYPERVISOR)..." 1589log "INFO" "Starting VM ($VCONTAINER_HYPERVISOR)..."
1443 1590
1444# Interactive mode runs VM in foreground with stdio connected 1591# Interactive mode runs VM in foreground with stdio connected