diff options
| -rw-r--r-- | recipes-containers/vcontainer/README.md | 54 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vcontainer-common.sh | 18 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vcontainer-init-common.sh | 54 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vdkr-init.sh | 58 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vpdmn-init.sh | 61 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vrunner.sh | 147 |
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`: | |||
| 317 | vdkr --secure-registry --ca-cert /path/to/ca.crt pull myimage | 317 | vdkr --secure-registry --ca-cert /path/to/ca.crt pull myimage |
| 318 | ``` | 318 | ``` |
| 319 | 319 | ||
| 320 | ### Passing an existing docker/podman auth file (`--config`) | ||
| 321 | |||
| 322 | If you already have credentials set up on the host (for example, from | ||
| 323 | running `docker login` locally), you can pass the resulting auth file | ||
| 324 | straight through into the emulated environment instead of re-entering | ||
| 325 | credentials with `--registry-user`/`--registry-pass`: | ||
| 326 | |||
| 327 | ```bash | ||
| 328 | # Docker (vdkr): uses ~/.docker/config.json by default | ||
| 329 | vdkr --config ~/.docker/config.json pull registry.example.com/myimage | ||
| 330 | |||
| 331 | # Podman (vpdmn): uses $XDG_RUNTIME_DIR/containers/auth.json | ||
| 332 | vpdmn --config $XDG_RUNTIME_DIR/containers/auth.json pull registry.example.com/myimage | ||
| 333 | ``` | ||
| 334 | |||
| 335 | The path can also be supplied via environment: | ||
| 336 | |||
| 337 | ```bash | ||
| 338 | export VDKR_CONFIG=$HOME/.docker/config.json | ||
| 339 | vdkr 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 | ||
| 322 | Mount host directories into containers using `-v` (requires memory resident mode): | 376 | 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} | |||
| 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" | |||
| 880 | CA_CERT="" | 886 | CA_CERT="" |
| 881 | REGISTRY_USER="" | 887 | REGISTRY_USER="" |
| 882 | REGISTRY_PASS="" | 888 | REGISTRY_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. | ||
| 893 | AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}" | ||
| 883 | COMMAND="" | 894 | COMMAND="" |
| 884 | COMMAND_ARGS=() | 895 | COMMAND_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 | |||
| 283 | AUTH_SHARE_TAG="" | ||
| 284 | AUTH_SHARE_MOUNT="/mnt/auth" | ||
| 285 | |||
| 286 | mount_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 | |||
| 311 | unmount_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. | ||
| 177 | install_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 |
| 685 | install_registry_ca | 738 | install_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. | ||
| 743 | install_auth_config | ||
| 744 | |||
| 687 | # Start containerd and dockerd (Docker-specific) | 745 | # Start containerd and dockerd (Docker-specific) |
| 688 | start_containerd | 746 | start_containerd |
| 689 | start_dockerd | 747 | 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 @@ | |||
| 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. | ||
| 119 | install_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 |
| 101 | stop_runtime_daemons() { | 158 | stop_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) |
| 191 | verify_podman | 248 | verify_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. | ||
| 252 | install_auth_config | ||
| 253 | |||
| 193 | # Handle daemon mode or single command execution | 254 | # Handle daemon mode or single command execution |
| 194 | if [ "$RUNTIME_DAEMON" = "1" ]; then | 255 | if [ "$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}}" | |||
| 38 | TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}" | 38 | TIMEOUT="${VDKR_TIMEOUT:-${VPDMN_TIMEOUT:-300}}" |
| 39 | VERBOSE="${VDKR_VERBOSE:-${VPDMN_VERBOSE:-false}}" | 39 | VERBOSE="${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. | ||
| 46 | AUTH_CONFIG="${VDKR_CONFIG:-${VPDMN_CONFIG:-}}" | ||
| 47 | |||
| 41 | # Runtime-specific settings (set after parsing --runtime) | 48 | # Runtime-specific settings (set after parsing --runtime) |
| 42 | set_runtime_config() { | 49 | set_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 | |||
| 847 | TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$" | 867 | TEMP_DIR="${TMPDIR:-/tmp}/vdkr-$$" |
| 848 | mkdir -p "$TEMP_DIR" | 868 | mkdir -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). | ||
| 887 | AUTH_SHARE_DIR="" | ||
| 888 | |||
| 889 | validate_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. | ||
| 956 | setup_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 | |||
| 850 | cleanup() { | 988 | cleanup() { |
| 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" |
| 1440 | fi | 1582 | fi |
| 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. | ||
| 1587 | setup_auth_share | ||
| 1588 | |||
| 1442 | log "INFO" "Starting VM ($VCONTAINER_HYPERVISOR)..." | 1589 | log "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 |
