Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/os-image.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OS Images

Official pre-built OCI VM images (Ubuntu, Android), the Windows artifact, and the `os-image/` build harness.
Official pre-built OCI VM images (Ubuntu, Debian, Android), the Windows artifact, and the `os-image/` build harness.

Pre-built OS images are hosted on [GitHub Container Registry](https://github.com/orgs/cocoonstack/packages?repo_name=cocoon).

Expand All @@ -18,6 +18,16 @@ Multi-arch (`linux/amd64`, `linux/arm64`).
| Ubuntu 24.04 + Xfce | `24.04-xface` | `ghcr.io/cocoonstack/cocoon/ubuntu:24.04-xface` |
| Ubuntu 24.04 + PicoClaw | `24.04-picoclaw` | `ghcr.io/cocoonstack/cocoon/ubuntu:24.04-picoclaw` |

### Debian

Multi-arch (`linux/amd64`, `linux/arm64`). Built from `os-image/debian/13/` with
the Debian 13 cloud kernel, Cocoon boot and networking hooks, and the
checksum-verified agent.

| Image | Tag | IMAGE_NAME |
|-------|-----|------------|
| Debian 13 (Trixie) | `13` | `ghcr.io/cocoonstack/cocoon/debian:13` |

### Android (Redroid)

`linux/amd64` only. Runs Android via [Redroid](https://github.com/remote-android/redroid-doc) directly as PID 1 in the VM — no Ubuntu/systemd layer.
Expand Down
59 changes: 59 additions & 0 deletions os-image/debian/13/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Use the latest Debian 13 (Trixie)
FROM docker.io/library/debian:13

ARG TARGETARCH
ENV DEBIAN_FRONTEND=noninteractive

# Combined System Setup & Optimization (Single Layer)
# Install packages first so /etc/initramfs-tools/scripts/ exists, then inject the hook.
RUN --mount=type=secret,id=cocoon_overlay \
--mount=type=secret,id=cocoon_network \
--mount=type=secret,id=cocoon_install_agent \
apt-get update && apt-get install -y --no-install-recommends \
linux-image-cloud-${TARGETARCH} \
initramfs-tools \
# initramfs-tools only recommends busybox and --no-install-recommends skips
# recommends; without it the initramfs has only klibc-utils (no tr, and
# klibc mount rejects options after operands).
busybox \
systemd \
systemd-sysv \
systemd-timesyncd \
systemd-resolved \
udev \
kmod \
iproute2 iputils-ping curl wget arping tcpdump \
openssh-server \
ca-certificates \
&& \
cp /run/secrets/cocoon_overlay /etc/initramfs-tools/scripts/cocoon-overlay && \
chmod 0755 /etc/initramfs-tools/scripts/cocoon-overlay && \
cp /run/secrets/cocoon_network /etc/initramfs-tools/scripts/init-bottom/cocoon-network && \
chmod 0755 /etc/initramfs-tools/scripts/init-bottom/cocoon-network && \
# [Kernel Setup] Force critical modules and set gzip compression
printf "erofs\noverlay\next4\nvirtio_blk\nvirtio_pci\nvirtio_ring\nvirtio_net\nvsock\nvmw_vsock_virtio_transport\n" >> /etc/initramfs-tools/modules && \
sed -i 's/^COMPRESS=.*/COMPRESS=gzip/' /etc/initramfs-tools/initramfs.conf && \
# [Networking] IP=off prevents initramfs from running DHCP during boot.
# Kernel ip= parameters (when present) override this and still trigger ipconfig.
# Without this, IP="" (default) causes dhcpcd to run unconditionally, which
# blocks boot on --nics 0 VMs and persists DHCP leases as static configs.
sed -i '/^IP=/d' /etc/initramfs-tools/initramfs.conf && \
echo 'IP=off' >> /etc/initramfs-tools/initramfs.conf && \
# Regenerate initramfs inside this layer (consumes the secret script)
update-initramfs -u -k all && \
# [Shift-Left Hacks] Neuter systemd to avoid boot-time filesystem checks
truncate -s 0 /etc/fstab && \
systemctl mask systemd-fsck-root.service systemd-remount-fs.service systemd-fsck@.service && \
# [Networking] Enable networkd/resolved and configure DHCP
systemctl enable systemd-networkd systemd-resolved systemd-timesyncd && \
mkdir -p /etc/systemd/network && \
printf "[Match]\nName=e* v*\n[Network]\nDHCP=yes\n\n[DHCPv4]\nClientIdentifier=mac\n" > /etc/systemd/network/20-wired.network && \
# [Cocoon agent + sshd] vsock exec daemon and SSH access.
sh /run/secrets/cocoon_install_agent && \
# [Access] Set root password
echo 'root:cocoon' | chpasswd && \
# [Cleanup] Purge APT cache to minimize EROFS size
rm -rf /var/lib/apt/lists/*

# Final configuration
CMD ["/sbin/init"]
48 changes: 48 additions & 0 deletions os-image/debian/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Cocoon-Compatible Debian 13 Image

This directory builds a bootable Debian 13 (`trixie`) OCI image for Cocoon on
`linux/amd64` and `linux/arm64`. Each platform includes its matching Debian
cloud kernel and checksum-verified Cocoon agent, plus the shared initramfs,
EROFS/overlay boot hooks, systemd networking, and SSH configuration.

## Build and Validate Locally
Comment thread
ppmzhang2 marked this conversation as resolved.

The validator uses Podman directly when it is available and otherwise falls
back to Docker; set `CONTAINER_ENGINE=podman` or `CONTAINER_ENGINE=docker` to
select one explicitly. Building or validating a non-native platform also
requires QEMU user emulation with the corresponding `binfmt` handler
registered; Podman Desktop normally provides this, while Linux Podman hosts
must configure it separately.

Build each platform with its own tag so Podman stores both architecture-specific
images locally. Run from the repository root:

```bash
AMD64_IMAGE=cocoon-debian-13:local-amd64

podman build \
--platform linux/amd64 \
--no-cache \
--file os-image/debian/13/Dockerfile \
--secret id=cocoon_overlay,src=os-image/debian/overlay.sh \
--secret id=cocoon_network,src=os-image/debian/network.sh \
--secret id=cocoon_install_agent,src=os-image/debian/install-agent.sh \
--tag "$AMD64_IMAGE" \
os-image/debian

os-image/debian/validate-image.sh "$AMD64_IMAGE"

ARM64_IMAGE=cocoon-debian-13:local-arm64

podman build \
--platform linux/arm64 \
--no-cache \
--file os-image/debian/13/Dockerfile \
--secret id=cocoon_overlay,src=os-image/debian/overlay.sh \
--secret id=cocoon_network,src=os-image/debian/network.sh \
--secret id=cocoon_install_agent,src=os-image/debian/install-agent.sh \
--tag "$ARM64_IMAGE" \
os-image/debian

os-image/debian/validate-image.sh "$ARM64_IMAGE"
```
62 changes: 62 additions & 0 deletions os-image/debian/install-agent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/bin/sh
# Vendored from cocoon/os-image/ubuntu/install-agent.sh at Cocoon v0.5.9
# (144927060c3e90dbe2f3e1a15143572c402958de).
# Install cocoon-agent (vsock exec) and sshd into a Debian/Ubuntu image.
# Caller is expected to have already installed `openssh-server` via apt
# in the same RUN, and to have curl available.
#
# Idempotent: re-running the script overwrites the binary and unit file,
# `systemctl enable` is a no-op when the symlinks are already in place.
set -eu

AGENT_VERSION="${COCOON_AGENT_VERSION:-0.2.0}"
ARCH="${TARGETARCH:-$(dpkg --print-architecture)}"
case "$ARCH" in
amd64) AGENT_ARCH="x86_64"; AGENT_SHA256="73dc18e588828630f0ad5f3a1b86511f315d0df1f2efcbb409e821d258d45234" ;;
arm64) AGENT_ARCH="arm64"; AGENT_SHA256="548f4729a11797c4a6abe1b65d3791606a9a4f1de5592bf2ea367c573e0f6768" ;;
*) echo "install-agent: unsupported arch '$ARCH'" >&2; exit 1 ;;
esac

# 1. sshd: permit root login (cocoon images use root:cocoon by default).
mkdir -p /run/sshd
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
systemctl enable ssh

# 2. cocoon-agent binary: pinned-version tarball from upstream releases.
# Per-arch SHA256 — bumping AGENT_VERSION without updating both checksums
# fails the sha256sum -c check instead of silently shipping a wrong binary.
TARBALL="cocoon-agent_${AGENT_VERSION}_Linux_${AGENT_ARCH}.tar.gz"
URL="https://github.com/cocoonstack/cocoon-agent/releases/download/v${AGENT_VERSION}/${TARBALL}"
TMP_TARBALL="$(mktemp)"
trap 'rm -f "$TMP_TARBALL"' EXIT
curl -fsSL "$URL" -o "$TMP_TARBALL"
echo "$AGENT_SHA256 $TMP_TARBALL" | sha256sum -c -
tar -xz -C /usr/local/bin/ -f "$TMP_TARBALL" cocoon-agent
chmod 0755 /usr/local/bin/cocoon-agent

# 3. systemd unit. Mirrors upstream packaging/cocoon-agent.service so the
# in-VM service stays in sync with what cocoon-agent is tested against.
cat > /etc/systemd/system/cocoon-agent.service <<'EOF'
[Unit]
Description=Cocoon agent (vsock command exec)
Documentation=https://github.com/cocoonstack/cocoon-agent

[Service]
Type=simple
User=root
Group=root
# Best-effort load — most kernels build the transport in or auto-load on
# virtio-vsock device probe; the leading dash keeps the unit alive on
# minimal kernels (e.g. ubuntu linux-image-virtual).
ExecStartPre=-/sbin/modprobe vhost_vsock
ExecStart=/usr/local/bin/cocoon-agent serve
Environment=AGENT_LOG_LEVEL=info
Restart=always
RestartSec=2s
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

systemctl enable cocoon-agent.service
110 changes: 110 additions & 0 deletions os-image/debian/network.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/bin/sh
# Vendored from cocoon/os-image/ubuntu/network.sh at Cocoon v0.5.9
Comment thread
ppmzhang2 marked this conversation as resolved.
# (144927060c3e90dbe2f3e1a15143572c402958de).
# Target path: /etc/initramfs-tools/scripts/init-bottom/cocoon-network
#
# Runs in init-bottom phase — AFTER configure_networking has parsed kernel ip=
# parameters into /run/net-*.conf, and AFTER mountroot has assembled the overlay.
# Converts initramfs network config into systemd-networkd .network files so
# the IP configuration persists after switch_root, and writes /etc/resolv.conf
# for immediate DNS availability regardless of init system.

PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in prereqs) prereqs; exit 0 ;; esac

. /scripts/functions

# $rootmnt is set by initramfs — points to the mounted root filesystem.
[ -z "$rootmnt" ] && exit 0

# Set hostname from cocoon.hostname= kernel parameter.
for _arg in $(cat /proc/cmdline); do
case "$_arg" in
cocoon.hostname=*) echo "${_arg#cocoon.hostname=}" > "${rootmnt}/etc/hostname" ;;
esac
done

_dns_servers=""
_has_static=false

for conf_file in /run/net-*.conf; do
[ -f "$conf_file" ] || continue

unset DEVICE IPV4ADDR IPV4NETMASK IPV4GATEWAY IPV4DNS0 IPV4DNS1 HOSTNAME HWADDR
. "$conf_file"
[ -z "$DEVICE" ] && continue
[ -z "$IPV4ADDR" ] && continue

# Read MAC from sysfs if HWADDR not in conf (older klibc).
[ -z "$HWADDR" ] && [ -e "/sys/class/net/${DEVICE}/address" ] && HWADDR=$(cat "/sys/class/net/${DEVICE}/address")
[ -z "$HWADDR" ] && continue

_has_static=true

# Convert dotted netmask to prefix length.
prefix=0
IFS=. read -r a b c d <<EOF
${IPV4NETMASK}
EOF
for octet in $a $b $c $d; do
case $octet in
255) prefix=$((prefix + 8)) ;;
254) prefix=$((prefix + 7)) ;;
252) prefix=$((prefix + 6)) ;;
248) prefix=$((prefix + 5)) ;;
240) prefix=$((prefix + 4)) ;;
224) prefix=$((prefix + 3)) ;;
192) prefix=$((prefix + 2)) ;;
128) prefix=$((prefix + 1)) ;;
esac
done

# Use MAC-based matching so the config works regardless of device naming
# (eth0, enp0s4, or any name after hot-swap). File name uses MAC without
# colons to avoid collisions with old device-name-based files.
mac_sanitized=$(echo "$HWADDR" | tr -d ':')
mkdir -p "${rootmnt}/etc/systemd/network"
{
printf "[Match]\nMACAddress=%s\n\n[Network]\nAddress=%s/%d\n" "$HWADDR" "$IPV4ADDR" "$prefix"
[ -n "$IPV4GATEWAY" ] && [ "$IPV4GATEWAY" != "0.0.0.0" ] && printf "Gateway=%s\n" "$IPV4GATEWAY"
[ -n "$IPV4DNS0" ] && [ "$IPV4DNS0" != "0.0.0.0" ] && printf "DNS=%s\n" "$IPV4DNS0"
[ -n "$IPV4DNS1" ] && [ "$IPV4DNS1" != "0.0.0.0" ] && printf "DNS=%s\n" "$IPV4DNS1"
# Fallback DNS if none provided.
if [ -z "$IPV4DNS0" ] || [ "$IPV4DNS0" = "0.0.0.0" ]; then
printf "DNS=8.8.8.8\nDNS=8.8.4.4\n"
fi
} > "${rootmnt}/etc/systemd/network/10-${mac_sanitized}.network"

# Collect DNS servers for resolv.conf.
[ -n "$IPV4DNS0" ] && [ "$IPV4DNS0" != "0.0.0.0" ] && _dns_servers="${_dns_servers} ${IPV4DNS0}"
[ -n "$IPV4DNS1" ] && [ "$IPV4DNS1" != "0.0.0.0" ] && _dns_servers="${_dns_servers} ${IPV4DNS1}"

done

# Fallback: no kernel ip= configured — write DHCP config per NIC matched by MAC.
# This covers macvlan / external DHCP scenarios where CNI does not assign IPs.
if [ "$_has_static" = false ]; then
mkdir -p "${rootmnt}/etc/systemd/network"
for sysdev in /sys/class/net/*; do
[ -e "$sysdev" ] || continue
dev=$(basename "$sysdev")
# Skip loopback and virtual devices.
case "$dev" in lo|bonding_masters) continue ;; esac
[ -e "${sysdev}/address" ] || continue
mac=$(cat "${sysdev}/address")
# Skip zero/empty MACs.
case "$mac" in ""|00:00:00:00:00:00) continue ;; esac
mac_sanitized=$(echo "$mac" | tr -d ':')
{
printf "[Match]\nMACAddress=%s\n\n[Network]\nDHCP=ipv4\n\n[DHCPv4]\nClientIdentifier=mac\n" "$mac"
} > "${rootmnt}/etc/systemd/network/10-${mac_sanitized}.network"
done
fi

# Write /etc/resolv.conf from DNS servers collected above.
[ -z "$_dns_servers" ] && _dns_servers="8.8.8.8 8.8.4.4"
: > "${rootmnt}/etc/resolv.conf"
for _ns in $_dns_servers; do
printf "nameserver %s\n" "$_ns" >> "${rootmnt}/etc/resolv.conf"
done
Loading