# Plan: Debian Package Source for linux-cloud-router ## Context The `azure-router` Terraform module provisions a Linux router VM using Cloud Init. Cloud Init installs packages, writes config files (strongSwan swanctl, WireGuard, netplan, sysctl, UFW rules), and runs a one-shot setup service. All site-specific values (IPs, FQDNs, CIDRs, PSK) are injected at provision time via Terraform's `templatefile()`. The Debian package replicates the same outcome on Ubuntu 22.04, 24.04, and 26.04 hosts (exact supported version list TBD — compatibility and feature availability must be verified across all three; see Further Considerations). Site-specific values are collected via **debconf** interactive prompts at `dpkg install` time. A `postinst` script reads the debconf answers and generates all derived config files. --- ## Decisions - Site config: **debconf** interactive prompts at install time - P2S certificates: managed with `simple-ca.sh` directly - Networking: Ubuntu only — netplan (not `/etc/network/interfaces.d/`) - Build approach: full Debian source package (`dpkg-buildpackage` + debhelper ≥13) - PKI library: `simple-ca.sh` (OpenSSL-based); currently RSA-4096, ECDSA/Ed25519 support is a planned future enhancement to the script itself — not a concern for this package - `strongswan-pki` is **not** a dependency; `openssl` covers all PKI needs (including PKCS#12 which `pki` tool cannot produce) --- ## Source Tree Layout ``` linux-cloud-router/ ├── debian/ │ ├── control │ ├── changelog │ ├── rules │ ├── copyright │ ├── install │ ├── dirs │ ├── templates ← debconf question definitions │ ├── config ← pre-install debconf prompts │ ├── postinst ← generates all config files, enables services │ └── prerm ← stops/disables services └── src/ ├── etc/ │ ├── sysctl.d/ │ │ └── 99-cloud-router.conf │ ├── xdg/nvim/ │ │ └── init.vim │ └── systemd/system/ │ └── cloud-router-setup.service └── usr/local/ ├── lib/ │ └── simple-ca.sh └── sbin/ └── cloud-router-setup adapted from azure-router-setup ``` --- ## Phase 0 — Package Availability Research Use Apple's `container` CLI to launch one-shot Ubuntu containers for each target release: ```sh PKGS="strongswan-swanctl charon-systemd libstrongswan-extra-plugins \ libcharon-extra-plugins wireguard-tools ufw openssl debconf \ debhelper netplan.io" for RELEASE in 22.04 24.04 26.04; do echo "=== ubuntu:$RELEASE ===" container run --rm ubuntu:$RELEASE \ sh -c "apt-get update -qq && apt-cache policy $PKGS" done ``` `container` is also the primary build environment: mount the source tree and run `dpkg-buildpackage` inside the target Ubuntu container, keeping the macOS host clean. Record the candidate version for each package per release. Use the results to: - Confirm all packages exist in the default repos (no PPAs needed) - Verify `debhelper` version supports compat 14 (requires ≥13.10); fall back to compat 13 if 22.04 support is required and its debhelper is too old - Confirm strongSwan package names are consistent across releases - Finalise the supported Ubuntu version list --- ## Phase 1 — Package Scaffold 1. `debian/control` — package name `cloud-router`, short description, `Build-Depends` and `Depends` (see below) 2. `debian/changelog` — initial `1.0.0-1` entry 3. `debian/rules` — minimal `dh $@` (debhelper) 4. `debian/copyright` — MIT (matches simple-ca.sh) **`debian/control` Build-Depends:** ``` debhelper-compat (= 14) ``` **`debian/control` Depends:** ``` strongswan-swanctl, charon-systemd, libstrongswan-extra-plugins, libcharon-extra-plugins, wireguard-tools, ufw, debconf, openssl ``` --- ## Phase 2 — Static Source Files 6. `src/etc/sysctl.d/99-cloud-router.conf` ``` net.ipv4.ip_forward = 1 net.ipv4.conf.all.rp_filter = 0 net.ipv4.conf.default.rp_filter = 0 ``` (identical content to cloud-init) 7. `src/etc/xdg/nvim/init.vim` — exact neovim settings from `router-cloud-init.tpl` 8. `src/etc/systemd/system/cloud-router-setup.service` Renamed from `azure-router-setup.service`. Same content: ```ini [Unit] Description=Linux Cloud Router Setup Wants=network-online.target After=network-online.target [Service] Type=oneshot ExecStart=/usr/local/sbin/cloud-router-setup RemainAfterExit=yes [Install] WantedBy=multi-user.target ``` --- ## Phase 3 — `cloud-router-setup` Script 9. Adapt `azure-router-setup` bash script from `router-cloud-init.tpl`: - Add `source /etc/default/cloud-router` at the top - Replace all Terraform-interpolated values (`${local_subnet}`, `${remote_cidrs}`, `${wg_listen_port}`, `${p2s_address_pool}`, `${local_addrs}`, `${fqdn}`) with the corresponding variable names sourced from `/etc/default/cloud-router` - Rename script to `cloud-router-setup`; install to `src/usr/local/sbin/cloud-router-setup` - The script remains idempotent (checks for existing UFW rule markers before inserting) - WireGuard key generation and WireGuard UFW rules are guarded by `if [ "$WG_ENABLED" = "true" ]` --- ## Phase 4 — PKI Library 10. `src/usr/local/lib/simple-ca.sh` — Bash function module providing `make_ca`, `make_cert`, `make_pfx`. Source it in any shell session with `source /usr/local/lib/simple-ca.sh`; set `SIMPLE_CA_DIR=/etc/cloud-router/pki` before use. 11. `cloud-router-sync-pki` — standalone script that copies PKI output to swanctl's expected locations: - `/etc/cloud-router/pki/ca_bundle.pem` → `/etc/swanctl/x509ca/ca.pem` - `/etc/cloud-router/pki/{server}_cert.pem` → `/etc/swanctl/x509/server.pem` - `/etc/cloud-router/pki/{server}_key.pem` → `/etc/swanctl/private/server.key` The server cert name is read from `P2S_SERVER_NAME` in `/etc/default/cloud-router` (defaults to the hostname component of `LOCAL_FQDN`). --- ## Phase 5 — Debconf Templates and Config Script 13. `debian/templates` — one `Template:` block per configurable value: | Question | Type | Default | |---|---|---| | `cloud-router/local_addrs` | string | | | `cloud-router/local_fqdn` | string | | | `cloud-router/local_id_mode` | select | `fqdn` (choices: fqdn, public_ip, internal_ip) | | `cloud-router/local_cidrs` | string | | | `cloud-router/remote_addrs` | string | | | `cloud-router/remote_id` | string | | | `cloud-router/psk` | password | | | `cloud-router/remote_cidrs` | string | | | `cloud-router/router_int_gateway_ip` | string | | | `cloud-router/p2s_address_pool` | string | | | `cloud-router/wg_enabled` | boolean | `false` | | `cloud-router/wg_address` | string | `10.0.1.1/24` *(shown only when wg_enabled=true)* | | `cloud-router/wg_listen_port` | string | `51820` *(shown only when wg_enabled=true)* | 14. `debian/config` — pre-install shell script: - Sources `/usr/share/debconf/confmodule` - Calls `db_input` for each question in priority order - Shows `wg_address` and `wg_listen_port` conditionally: `db_get cloud-router/wg_enabled; if [ "$RET" = "true" ]` --- ## Phase 6 — Maintainer Scripts 15. `debian/postinst` (triggered on `configure`): 1. `db_get` all debconf answers 2. Derive `LOCAL_SUBNET` = first element of `LOCAL_CIDRS` 3. Derive `P2S_SERVER_NAME` = hostname part of `LOCAL_FQDN` 4. Write `/etc/default/cloud-router` (mode 0644, root:root) — non-secret config: ```sh LOCAL_ADDRS="..." LOCAL_FQDN="..." LOCAL_ID_MODE="..." LOCAL_CIDRS="..." LOCAL_SUBNET="..." REMOTE_ADDRS="..." REMOTE_ID="..." REMOTE_CIDRS="..." ROUTER_INT_GATEWAY_IP="..." P2S_ADDRESS_POOL="..." P2S_SERVER_NAME="..." WG_ENABLED="..." WG_ADDRESS="..." # only relevant when WG_ENABLED=true WG_LISTEN_PORT="..." # only relevant when WG_ENABLED=true ``` 5. Generate `/etc/swanctl/conf.d/remote-site.conf` (mode 0600, root:root) via bash heredoc — includes both the `connections {}` block and a `secrets { ike-remote-site { secret = "$PSK" } }` block; the file must be 0600 because it contains the PSK 6. Generate `/etc/swanctl/conf.d/road-warrior.conf` via heredoc 7. Generate `/etc/systemd/resolved.conf.d/p2s-forwarder.conf` via heredoc 8. If `WG_ENABLED=true`: generate `/etc/wireguard/wg0.conf` via heredoc 9. Generate `/etc/netplan/90-cloud-router.yaml` via heredoc 10. `sysctl --system` 11. `netplan apply` 12. `systemctl daemon-reload` 13. `systemctl enable --now cloud-router-setup.service` 14. `ufw allow 22/tcp && ufw --force enable && ufw reload` 15. `systemctl enable --now strongswan` 16. `systemctl restart systemd-resolved` 16. `debian/prerm` (on `remove`): - `systemctl disable --now cloud-router-setup.service` - `systemctl disable --now strongswan` --- ## Phase 7 — Install Manifest 17. `debian/install` — maps `src/` tree entries to absolute filesystem paths, e.g.: ``` src/etc/sysctl.d/99-cloud-router.conf etc/sysctl.d/ src/etc/xdg/nvim/init.vim etc/xdg/nvim/ src/etc/systemd/system/cloud-router-setup.service etc/systemd/system/ src/usr/local/sbin/cloud-router-setup usr/local/sbin/ src/usr/local/lib/simple-ca.sh usr/local/lib/ ``` 18. `debian/dirs` — directories that must pre-exist: ``` etc/cloud-router etc/cloud-router/pki etc/wireguard etc/swanctl/conf.d etc/swanctl/x509ca etc/swanctl/x509 etc/swanctl/private etc/systemd/resolved.conf.d ``` --- ## Phase 8 — Cloud-Init YAML The `.deb` is delivered via a private apt repository (URL/GPG key TBD). The cloud-init user-data pre-seeds all debconf answers before the package is installed, so `postinst` runs fully unattended on first boot. Template file `cloud-router-cloud-init.yaml.tpl` (rendered by `templatefile()` at provision time, Terraform variable names shown as `${…}`): ```yaml #cloud-config apt: sources: cloud-router: source: "deb [signed-by=/etc/apt/keyrings/cloud-router.gpg] ${repo_url} ${ubuntu_codename} main" key: | ${repo_gpg_key} debconf_selections: | cloud-router cloud-router/local_addrs string ${local_addrs} cloud-router cloud-router/local_fqdn string ${fqdn} cloud-router cloud-router/local_id_mode select ${local_id_mode} cloud-router cloud-router/local_cidrs string ${local_cidrs} cloud-router cloud-router/remote_addrs string ${remote_addrs} cloud-router cloud-router/remote_id string ${remote_id} cloud-router cloud-router/psk password ${psk} cloud-router cloud-router/remote_cidrs string ${remote_cidrs} cloud-router cloud-router/router_int_gateway_ip string ${router_int_gateway_ip} cloud-router cloud-router/p2s_address_pool string ${p2s_address_pool} cloud-router cloud-router/wg_enabled boolean ${wg_enabled} cloud-router cloud-router/wg_address string ${wg_address} cloud-router cloud-router/wg_listen_port string ${wg_listen_port} package_update: true packages: - cloud-router ``` Notes: - `debconf_selections` runs before `packages` in cloud-init's module order, so answers are in place before `postinst` executes - `DEBIAN_FRONTEND` does not need to be set explicitly; debconf detects the pre-seeded answers and skips interactive prompts automatically - `${ubuntu_codename}` (e.g. `noble`) must be passed as a Terraform variable or derived from the VM image; `${repo_gpg_key}` is the ASCII-armoured public key of the signing key - The template replaces the existing `router-cloud-init.tpl` in the `azure-router` module --- ## Verification 1. `dpkg-buildpackage -us -uc -b` from `linux-cloud-router/` completes without error; `.deb` produced 2. `dpkg -i cloud-router_1.0.0-1_all.deb` on a fresh VM for each target Ubuntu release (22.04, 24.04, 26.04): debconf prompts appear; all deps satisfied 3. `cat /etc/default/cloud-router` shows non-secret config; `/etc/swanctl/conf.d/remote-site.conf` and `road-warrior.conf` are present; `remote-site.conf` is mode 0600 and contains the `secrets {}` block with the PSK; `/etc/wireguard/wg0.conf` exists only when `WG_ENABLED=true` 4. Source `simple-ca.sh`, then `make_ca "My Router CA"` → `/etc/cloud-router/pki/ca_cert.pem` created; `make_cert --type server router.example.com` → cert/key produced; `cloud-router-sync-pki` → files appear in `/etc/swanctl/` 5. `make_pfx ` → `.pfx` produced for a road-warrior client 6. `systemctl status cloud-router-setup.service` is `active (exited)`; UFW `before.rules` contains the IPsec/NAT/forwarding sections; when `WG_ENABLED=true`, WireGuard keys are present at `/etc/wireguard/wg0.key` and `wg0.pub` --- ## Further Considerations - **Ubuntu version compatibility** (must verify before finalising supported version list): items to check on each of 22.04, 24.04, 26.04: - `debhelper-compat (= 14)` — Ubuntu 22.04 ships debhelper 13.6 (max compat 13); 24.04 ships 13.11 (compat 14 supported); may need to drop to compat 13 for 22.04 support - strongSwan package names (`strongswan-swanctl`, `charon-systemd`, plugin packages) — verify availability and correct names on each release - `wireguard-tools` availability and kernel module inclusion - netplan API and YAML schema changes between releases - `systemd-resolved` DNS stub behaviour differences - **`dpkg-reconfigure` behaviour**: re-running `dpkg-reconfigure cloud-router` re-prompts all debconf questions and regenerates config files, overwriting any manual edits. Files "owned" by debconf (remote-site.conf, wg0.conf, netplan yaml, env) should be documented as managed; swanctl cert files (written via `simple-ca.sh`) are not touched by postinst. - **`simple-ca.sh` key algorithm support**: currently RSA-4096 only. ECDSA/Ed25519 support is a planned future enhancement to `simple-ca.sh` itself and is independent of this package. - **CRL / revocation**: `simple-ca.sh` has no revocation support. If road-warrior client revocation is needed, `strongswan-pki` can be added as an optional dependency for `pki --signcrl`. Treat as a future enhancement. - **Build tooling**: `container` (Apple container CLI) is used to run `dpkg-buildpackage` inside Ubuntu containers. The source tree is bind-mounted into the container; the built `.deb` appears in the parent directory as usual. Inside the container: `apt-get install -y build-essential debhelper`. No Ubuntu host or VM needed for builds. - **Development environment**: source files are authored on macOS. `container` handles all build and install testing — run the same loop against 22.04, 24.04, and 26.04 containers once the compatibility matrix is resolved.