Created
May 12, 2026 09:04
-
-
Save sawirricardo/4210f43918afedaaca9e9ff8dc65816d to your computer and use it in GitHub Desktop.
Cloud Init Config
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #cloud-config | |
| package_update: true | |
| package_upgrade: true | |
| package_reboot_if_required: false | |
| ssh_pwauth: false | |
| write_files: | |
| - path: /etc/bootstrap-secrets.env | |
| owner: root:root | |
| permissions: '0600' | |
| content: | | |
| # Fill these before using this cloud-init file. | |
| # Leave DEV_USER empty to use the image's UID 1000 user, or create/use "dev". | |
| DEV_USER="" | |
| # Tailscale: generate a preferably tagged, pre-approved auth key. | |
| TS_AUTH_KEY="" | |
| TAILSCALE_HOSTNAME="" | |
| TAILSCALE_EXTRA_ARGS="" | |
| # Cloudflare Tunnel: token from Zero Trust > Networks > Tunnels. | |
| CF_TUNNEL_TOKEN="" | |
| # Optional non-interactive auth for Codex and Claude Code. | |
| OPENAI_API_KEY="" | |
| ANTHROPIC_API_KEY="" | |
| # Host maintenance and hardening. | |
| UNATTENDED_REBOOT="true" | |
| UNATTENDED_REBOOT_TIME="04:00" | |
| # Optional swap for small VMs. Use an integer number of GB, or 0 to disable. | |
| SWAP_SIZE_GB="0" | |
| - path: /usr/local/sbin/bootstrap-dev-box.sh | |
| owner: root:root | |
| permissions: '0755' | |
| content: | | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| source /etc/bootstrap-secrets.env | |
| export DEBIAN_FRONTEND=noninteractive | |
| . /etc/os-release | |
| if [ "${ID}" != "ubuntu" ]; then | |
| echo "This cloud-init template expects Ubuntu. Detected ${PRETTY_NAME}." >&2 | |
| exit 1 | |
| fi | |
| apt-get update | |
| apt-get install -y ca-certificates curl gnupg git jq lsb-release sudo unzip build-essential unattended-upgrades apt-listchanges needrestart | |
| # Keep security updates flowing without interactive prompts. | |
| cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF' | |
| APT::Periodic::Update-Package-Lists "1"; | |
| APT::Periodic::Download-Upgradeable-Packages "1"; | |
| APT::Periodic::AutocleanInterval "7"; | |
| APT::Periodic::Unattended-Upgrade "1"; | |
| EOF | |
| cat >/etc/apt/apt.conf.d/52unattended-upgrades-local <<EOF | |
| Unattended-Upgrade::Allowed-Origins { | |
| "\${distro_id}:\${distro_codename}"; | |
| "\${distro_id}:\${distro_codename}-security"; | |
| "\${distro_id}ESMApps:\${distro_codename}-apps-security"; | |
| "\${distro_id}ESM:\${distro_codename}-infra-security"; | |
| }; | |
| Unattended-Upgrade::Remove-New-Unused-Dependencies "true"; | |
| Unattended-Upgrade::Remove-Unused-Dependencies "true"; | |
| Unattended-Upgrade::Automatic-Reboot "${UNATTENDED_REBOOT:-true}"; | |
| Unattended-Upgrade::Automatic-Reboot-Time "${UNATTENDED_REBOOT_TIME:-04:00}"; | |
| Unattended-Upgrade::SyslogEnable "true"; | |
| EOF | |
| cat >/etc/apt/listchanges.conf <<'EOF' | |
| [apt] | |
| frontend=none | |
| confirm=false | |
| which=news | |
| EOF | |
| install -d /etc/needrestart/conf.d | |
| cat >/etc/needrestart/conf.d/99-autorestart.conf <<'EOF' | |
| $nrconf{restart} = 'a'; | |
| $nrconf{kernelhints} = 0; | |
| EOF | |
| systemctl enable --now apt-daily.timer apt-daily-upgrade.timer || true | |
| # Bound local log growth. | |
| install -d /etc/systemd/journald.conf.d | |
| cat >/etc/systemd/journald.conf.d/90-limits.conf <<'EOF' | |
| [Journal] | |
| SystemMaxUse=512M | |
| RuntimeMaxUse=128M | |
| MaxRetentionSec=14day | |
| EOF | |
| systemctl restart systemd-journald || true | |
| # Harden SSH while keeping key-based access intact. | |
| install -d /etc/ssh/sshd_config.d | |
| cat >/etc/ssh/sshd_config.d/90-cloud-init-hardening.conf <<'EOF' | |
| PasswordAuthentication no | |
| KbdInteractiveAuthentication no | |
| PermitRootLogin prohibit-password | |
| X11Forwarding no | |
| MaxAuthTries 3 | |
| ClientAliveInterval 300 | |
| ClientAliveCountMax 2 | |
| EOF | |
| if command -v sshd >/dev/null 2>&1; then | |
| sshd -t | |
| systemctl reload ssh || systemctl reload sshd || true | |
| fi | |
| timedatectl set-ntp true || true | |
| if [[ "${SWAP_SIZE_GB:-0}" =~ ^[1-9][0-9]*$ ]] && [ ! -f /swapfile ]; then | |
| fallocate -l "${SWAP_SIZE_GB}G" /swapfile || dd if=/dev/zero of=/swapfile bs=1M count="$((SWAP_SIZE_GB * 1024))" | |
| chmod 600 /swapfile | |
| mkswap /swapfile | |
| swapon /swapfile | |
| echo '/swapfile none swap sw 0 0' >>/etc/fstab | |
| fi | |
| # Keep Docker JSON logs from filling the disk. | |
| install -d /etc/docker | |
| cat >/etc/docker/daemon.json <<'EOF' | |
| { | |
| "log-driver": "json-file", | |
| "log-opts": { | |
| "max-size": "10m", | |
| "max-file": "3" | |
| }, | |
| "live-restore": true | |
| } | |
| EOF | |
| # Docker Engine from Docker's official apt repository. | |
| install -m 0755 -d /etc/apt/keyrings | |
| curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc | |
| chmod a+r /etc/apt/keyrings/docker.asc | |
| cat >/etc/apt/sources.list.d/docker.sources <<EOF | |
| Types: deb | |
| URIs: https://download.docker.com/linux/ubuntu | |
| Suites: ${UBUNTU_CODENAME:-$VERSION_CODENAME} | |
| Components: stable | |
| Architectures: $(dpkg --print-architecture) | |
| Signed-By: /etc/apt/keyrings/docker.asc | |
| EOF | |
| # cloudflared from Cloudflare's apt repository. | |
| install -m 0755 -d /usr/share/keyrings | |
| curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg >/usr/share/keyrings/cloudflare-main.gpg | |
| chmod a+r /usr/share/keyrings/cloudflare-main.gpg | |
| echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main" >/etc/apt/sources.list.d/cloudflared.list | |
| apt-get update | |
| apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin cloudflared | |
| systemctl enable --now docker | |
| systemctl restart docker | |
| # Tailscale official Linux installer and optional unattended join. | |
| curl -fsSL https://tailscale.com/install.sh | sh | |
| systemctl enable --now tailscaled | |
| if [ -n "${TS_AUTH_KEY:-}" ]; then | |
| TS_ARGS=(up "--auth-key=${TS_AUTH_KEY}") | |
| if [ -n "${TAILSCALE_HOSTNAME:-}" ]; then | |
| TS_ARGS+=("--hostname=${TAILSCALE_HOSTNAME}") | |
| fi | |
| if [ -n "${TAILSCALE_EXTRA_ARGS:-}" ]; then | |
| # shellcheck disable=SC2206 | |
| TS_ARGS+=(${TAILSCALE_EXTRA_ARGS}) | |
| fi | |
| tailscale "${TS_ARGS[@]}" | |
| fi | |
| # Cloudflare Tunnel service, if a remotely-managed tunnel token is supplied. | |
| if [ -n "${CF_TUNNEL_TOKEN:-}" ]; then | |
| cloudflared service install "${CF_TUNNEL_TOKEN}" | |
| systemctl enable --now cloudflared | |
| fi | |
| # Pick the human user for AI CLI installs. | |
| if [ -z "${DEV_USER:-}" ]; then | |
| DEV_USER="$(getent passwd 1000 | cut -d: -f1 || true)" | |
| fi | |
| DEV_USER="${DEV_USER:-dev}" | |
| if ! id "${DEV_USER}" >/dev/null 2>&1; then | |
| useradd --create-home --shell /bin/bash --groups sudo "${DEV_USER}" | |
| echo "${DEV_USER} ALL=(ALL) NOPASSWD:ALL" >"/etc/sudoers.d/90-${DEV_USER}" | |
| chmod 0440 "/etc/sudoers.d/90-${DEV_USER}" | |
| fi | |
| usermod -aG docker "${DEV_USER}" || true | |
| DEV_HOME="$(getent passwd "${DEV_USER}" | cut -d: -f6)" | |
| install -d -o "${DEV_USER}" -g "${DEV_USER}" "${DEV_HOME}/.local/bin" "${DEV_HOME}/.config/ai-cli" | |
| sudo -H -u "${DEV_USER}" bash <<'USER_EOF' | |
| set -euo pipefail | |
| mkdir -p "$HOME/.local/bin" | |
| touch "$HOME/.profile" "$HOME/.bashrc" | |
| if ! grep -q 'HOME/.local/bin' "$HOME/.profile"; then | |
| printf '\nexport PATH="$HOME/.local/bin:$PATH"\n' >>"$HOME/.profile" | |
| fi | |
| if ! grep -q 'HOME/.local/bin' "$HOME/.bashrc"; then | |
| printf '\nexport PATH="$HOME/.local/bin:$PATH"\n' >>"$HOME/.bashrc" | |
| fi | |
| export PATH="$HOME/.local/bin:$PATH" | |
| curl -fsSL https://claude.ai/install.sh | bash | |
| curl -fsSL https://gist.githubusercontent.com/sawirricardo/47e94688398ea822908d81be3a30d811/raw/006a401d794feef8b6f1da7816881b7a42e2079b/install.sh | sh | |
| USER_EOF | |
| if [ -n "${OPENAI_API_KEY:-}" ] || [ -n "${ANTHROPIC_API_KEY:-}" ]; then | |
| { | |
| [ -n "${OPENAI_API_KEY:-}" ] && printf 'export OPENAI_API_KEY=%q\n' "${OPENAI_API_KEY}" | |
| [ -n "${ANTHROPIC_API_KEY:-}" ] && printf 'export ANTHROPIC_API_KEY=%q\n' "${ANTHROPIC_API_KEY}" | |
| } >"${DEV_HOME}/.config/ai-cli/env" | |
| chown "${DEV_USER}:${DEV_USER}" "${DEV_HOME}/.config/ai-cli/env" | |
| chmod 0600 "${DEV_HOME}/.config/ai-cli/env" | |
| if ! grep -q '.config/ai-cli/env' "${DEV_HOME}/.profile"; then | |
| printf '\n[ -f "$HOME/.config/ai-cli/env" ] && . "$HOME/.config/ai-cli/env"\n' >>"${DEV_HOME}/.profile" | |
| fi | |
| chown "${DEV_USER}:${DEV_USER}" "${DEV_HOME}/.profile" | |
| fi | |
| if [ -n "${OPENAI_API_KEY:-}" ]; then | |
| sudo -H -u "${DEV_USER}" bash -lc 'source "$HOME/.config/ai-cli/env"; printf "%s" "$OPENAI_API_KEY" | "$HOME/.local/bin/codex" login --with-api-key' | |
| fi | |
| ln -sf "${DEV_HOME}/.local/bin/codex" /usr/local/bin/codex | |
| ln -sf "${DEV_HOME}/.local/bin/claude" /usr/local/bin/claude | |
| docker --version | |
| tailscale version | |
| cloudflared --version | |
| sudo -H -u "${DEV_USER}" bash -lc 'codex --version && claude --version' | |
| runcmd: | |
| - [ bash, /usr/local/sbin/bootstrap-dev-box.sh ] | |
| final_message: "Docker, Tailscale, Cloudflare Tunnel, Codex, Claude Code, unattended upgrades, and host hardening finished. Check /var/log/cloud-init-output.log for details." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment