Skip to content

Instantly share code, notes, and snippets.

@sawirricardo
Created May 12, 2026 09:04
Show Gist options
  • Select an option

  • Save sawirricardo/4210f43918afedaaca9e9ff8dc65816d to your computer and use it in GitHub Desktop.

Select an option

Save sawirricardo/4210f43918afedaaca9e9ff8dc65816d to your computer and use it in GitHub Desktop.
Cloud Init Config
#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