FROM ubuntu:22.04@sha256:b8b6ee6aa931ecd9d0d952abc34dc0e5f7c6a30c6bb71b079fe399fde0329c02

ENV DEBIAN_FRONTEND=noninteractive

# Make apt resilient to flaky upstream (no apt-cacher-ng masking transient errors)
COPY config/apt/99retries.conf /etc/apt/apt.conf.d/99retries.conf

RUN apt-get update && apt-get install -y \
    systemd \
    systemd-sysv \
    openssh-server \
    iproute2 \
    iputils-ping \
    net-tools \
    dnsutils \
    procps \
    python3 \
    python3-apt \
    sqlite3 \
    jq \
    sudo \
    curl \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Trust the E2E forward proxy CA (for transparent HTTPS interception)
COPY config/proxy/squid-ca.crt /usr/local/share/ca-certificates/e2e-proxy-ca.crt
# Trust Pebble's test CA (so Caddy/ACME clients trust the simulated Let's Encrypt)
COPY config/pebble/pebble-ca.crt /usr/local/share/ca-certificates/pebble-ca.crt
RUN update-ca-certificates

# Node/npm ignore the OS trust store, so point them at the bundle that now
# includes the e2e CAs (mirrors Dockerfile.management). Set it in the image env
# AND /etc/environment so Ansible's SSH-invoked npm — a session that does NOT
# inherit Docker ENV — also picks it up via pam_env. Without this, a module's
# `npm install` against the npm-registry-sim fails SELF_SIGNED_CERT_IN_CHAIN.
# (openspec/specs/event-driven-hook-subscriptions/spec.md L6.)
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
RUN echo 'NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt' >> /etc/environment

RUN mkdir -p /run/sshd && \
    sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \
    sed -i 's/#PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config

# Enable SSH service
RUN systemctl enable ssh

# E2E network setup service (runs at boot to configure routing, DNS, SSH keys)
COPY config/routing/target-setup.sh /usr/local/bin/target-setup.sh
RUN chmod +x /usr/local/bin/target-setup.sh

COPY config/routing/target-setup.service /etc/systemd/system/target-setup.service
RUN systemctl enable target-setup

# Clean up unnecessary systemd services that slow down boot
RUN (cd /lib/systemd/system/sysinit.target.wants/ && \
    ls | grep -v systemd-tmpfiles-setup | xargs rm -f 2>/dev/null); \
    rm -f /lib/systemd/system/multi-user.target.wants/*; \
    rm -f /etc/systemd/system/*.wants/*; \
    rm -f /lib/systemd/system/local-fs.target.wants/*; \
    rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
    rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
    rm -f /lib/systemd/system/basic.target.wants/*; \
    true

# Re-enable the services we actually need
RUN systemctl enable ssh target-setup

# --- Caddy pre-installation ---
# Install Caddy (same steps as the Ansible role, but at image build time so
# the role finds it already installed and skips the slow cloudsmith.io downloads)
#
# `--no-install-recommends` is load-bearing here, not tidiness. `wireguard-tools`
# Recommends `wireguard-modules | wireguard-dkms`, and apt satisfies that with
# a KERNEL: `linux-modules-extra-*-realtime`, which drags in the realtime kernel
# image, its modules and `linux-firmware`. Measured 2026-09-05: that one line
# added 1.76 GB to this image (2.08 GB total), of which linux-firmware alone was
# 1.1 GB. A container cannot load a kernel module, and wg-quick already falls
# back to the userspace `wireguard-go` below, so none of it was ever used.
# The price of `--no-install-recommends` is that anything this image USED to get
# for free has to be named. Two things did, and both are load-bearing:
#
#   gnupg     — the very next step pipes the caddy signing key through `gpg
#               --dearmor`. Without it the build dies with `gpg: not found`,
#               which is how this was found: by building it.
#   iptables  — arrived via `wireguard-tools`' other Recommends
#               (`nftables | iptables`). The wireguard module's health check
#               runs `iptables-save` on the host it lands on, and wg-quick
#               consults `iptables-save` on teardown behind a `type -p` guard,
#               so its absence would not error — it would quietly do less.
RUN apt-get update && apt-get install -y --no-install-recommends \
    debian-keyring \
    debian-archive-keyring \
    apt-transport-https \
    gnupg \
    iptables \
    wireguard-tools \
    wireguard-go \
    && rm -rf /var/lib/apt/lists/*

# A provisioned box may host the `wireguard` module (the VPN does not have to
# terminate on the firewall). The module's validate_config requires wg to be
# present and fails loudly when it is not — so the stand-in for an operator's
# box carries it, exactly as a real one would. Userspace implementation named
# for the binary Ubuntu actually installs (/usr/bin/wireguard), not the package.
ENV WG_QUICK_USERSPACE_IMPLEMENTATION=wireguard
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
    gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
    tee /etc/apt/sources.list.d/caddy-stable.list
# Once caddy is installed the cloudsmith source has no job left — but the file
# must STAY: the Ansible role skips re-adding it on `creates:`, so deleting it
# only moves the curl into the deploy. Neutered to a comment instead, so no
# `apt-get update` in the sim waits on a repo whose only package is already
# installed here.
#
# The lists are deliberately NOT cleaned afterwards. target-setup used to run
# `apt-get update` at boot to guarantee Ansible found complete indexes, paying
# an external round-trip inside the 60s readiness budget for it; baking the
# indexes here buys the same guarantee off that path. A source that fails to
# refresh at deploy time keeps this copy. See #560.
RUN apt-get update && apt-get install -y caddy \
 && printf '# neutered at image build: caddy is pre-installed (see #560)\n' \
      > /etc/apt/sources.list.d/caddy-stable.list \
 && apt-get update

STOPSIGNAL SIGRTMIN+3
ENTRYPOINT ["/sbin/init"]
