From 71a0822286606e9e22fd1d388088858cc46fd353 Mon Sep 17 00:00:00 2001 From: dashprotocol <46986265+dashprotocol@users.noreply.github.com> Date: Mon, 25 May 2026 23:46:25 -0400 Subject: [PATCH] feat(H-005): add Lightsail provisioning/hardening scripts and D0 runbook --- README.md | 17 +++ docs/runbook/H-005-lightsail-provision.md | 126 ++++++++++++++++++++++ infra/create-instance.sh | 94 ++++++++++++++++ infra/provision.sh | 116 ++++++++++++++++++++ 4 files changed, 353 insertions(+) create mode 100644 docs/runbook/H-005-lightsail-provision.md create mode 100755 infra/create-instance.sh create mode 100755 infra/provision.sh diff --git a/README.md b/README.md index 70f23fb..66bd03d 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ Havenhold/ - npm 10+ - Docker Desktop +Optional for infrastructure provisioning (`feat/H-005` script path): +- AWS CLI v2 (`aws`) + ### 1. Install dependencies ```bash @@ -174,6 +177,20 @@ Frontend runs at `http://localhost:8080`, API runs at `http://localhost:3001`. - `npm run db:seed` - seed demo data - `npm run db:studio` - open Prisma Studio +## Infrastructure Provisioning (Optional) + +For Lightsail host setup in `feat/H-005`, you can use either: +- AWS Dashboard UI flow (no local AWS CLI dependency) +- CLI helper scripts in `infra/` (requires configured AWS CLI) + +CLI helper example: + +```bash +KEY_PAIR_NAME= ./infra/create-instance.sh +``` + +See `docs/runbook/H-005-lightsail-provision.md` for the full runbook and safety checks. + ## Security and Privacy Notes This project handles health-related data and should be treated carefully, even as an MVP. diff --git a/docs/runbook/H-005-lightsail-provision.md b/docs/runbook/H-005-lightsail-provision.md new file mode 100644 index 0000000..586c110 --- /dev/null +++ b/docs/runbook/H-005-lightsail-provision.md @@ -0,0 +1,126 @@ +# H-005 Runbook: Lightsail Provision + Host Hardening + +## Purpose +Provision a single Havenhold MVP Lightsail host and apply baseline hardening to satisfy deploy gate `D0`. + +## Security Handling +- This runbook is git-tracked and must stay sanitized. +- Do not commit secrets, private keys, account tokens, or operator-only CIDRs if sensitive. +- Store key material in approved team secret storage. + +## Prerequisites +- AWS account access to Lightsail in `us-east-1` +- Existing Lightsail-compatible SSH key pair name (public key registered) +- Local tools if using helper script: `aws` CLI configured +- Repo checked out locally + +## Resource Naming +- Instance: `havenhold-app-01` +- Static IP: `havenhold-app-ip` +- Admin user: `havenadmin` + +## Provision Path A (Recommended for this ticket): AWS Dashboard UI +1. In Lightsail (`us-east-1`), create instance: +- Platform: Linux/Unix +- Blueprint: Ubuntu `24.04 LTS` +- Plan: `small_3_0` +- Name: `havenhold-app-01` +- SSH key pair: select existing team key pair + +2. Allocate and attach static IP: +- Name: `havenhold-app-ip` +- Attach to `havenhold-app-01` + +3. Configure Lightsail firewall inbound rules: +- SSH: `22/tcp` (prefer operator CIDR allowlist) +- HTTP: `80/tcp` from internet +- HTTPS: `443/tcp` from internet + +## Provision Path B (Optional helper script) +From repo root: + +```bash +KEY_PAIR_NAME= ./infra/create-instance.sh +``` + +Optional custom SSH port: + +```bash +KEY_PAIR_NAME= SSH_PORT=2222 ./infra/create-instance.sh +``` + +## Hardening Step (Remote) +Copy and execute once on the VM: + +```bash +scp -i ./infra/provision.sh ubuntu@:/tmp/provision.sh +ssh -i ubuntu@ 'bash /tmp/provision.sh' +``` + +After script completion, set a local sudo password for the hardened admin user: + +```bash +ssh -t -i ubuntu@ 'sudo passwd havenadmin' +``` + +Optional custom SSH port on host: + +```bash +ssh -i ubuntu@ 'SSH_PORT=2222 bash /tmp/provision.sh' +``` + +## Critical Safety Sequence (Avoid Lockout) +1. Keep current SSH session open. +2. If SSH port changed, confirm Lightsail firewall includes new port first. +3. From local machine, open a second SSH session with hardened user: + +```bash +ssh -i -p havenadmin@ +``` + +4. Only close original session after second session succeeds. + +## D0 Verification Checklist +- [ ] Lightsail instance exists in `us-east-1` with static IP attached. +- [ ] Lightsail inbound rules expose only required ports (`22/80/443` or `2222/80/443`). +- [ ] `ssh -p havenadmin@` works with key auth. +- [ ] Root SSH login is rejected. +- [ ] Password SSH auth is rejected. +- [ ] UFW enabled with default deny inbound and only required ports allowed. +- [ ] `fail2ban` active with `sshd` jail loaded. +- [ ] `unattended-upgrades` enabled and healthy. +- [ ] This runbook + ticket plan are committed. + +## Evidence Commands +Run on host unless noted: + +```bash +# Firewall and listening state +sudo ufw status verbose +sudo ss -tulpen + +# SSH hardening +sudo sshd -T | egrep 'port|permitrootlogin|passwordauthentication|pubkeyauthentication|allowusers' + +# Update posture +sudo unattended-upgrade --dry-run --debug +systemctl status unattended-upgrades --no-pager + +# Abuse protection +sudo fail2ban-client status sshd +``` + +Optional from local machine: + +```bash +nmap -p ,80,443 +``` + +## Rollback / Recovery +- Take a Lightsail snapshot before major SSH/firewall changes. +- If locked out, use Lightsail browser-based access (if available) to restore SSH/UFW config. +- Re-open `22/tcp` temporarily in Lightsail firewall if custom port migration fails. + +## Notes +- Non-default SSH port reduces scan noise but does not replace strong key-only auth. +- Dual-layer controls are required: Lightsail firewall and UFW. diff --git a/infra/create-instance.sh b/infra/create-instance.sh new file mode 100755 index 0000000..824ad85 --- /dev/null +++ b/infra/create-instance.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional helper for H-005. +# This script uses AWS CLI to provision a Lightsail instance with a static IP and minimal public ports. +# Keep docs/runbook/H-005-lightsail-provision.md as source of truth. + +INSTANCE_NAME="${INSTANCE_NAME:-havenhold-app-01}" +STATIC_IP_NAME="${STATIC_IP_NAME:-havenhold-app-ip}" +REGION="${REGION:-us-east-1}" +AZ="${AZ:-us-east-1a}" +BLUEPRINT_ID="${BLUEPRINT_ID:-ubuntu_24_04}" +BUNDLE_ID="${BUNDLE_ID:-small_3_0}" +KEY_PAIR_NAME="${KEY_PAIR_NAME:-}" +SSH_PORT="${SSH_PORT:-22}" +SSH_CIDR="${SSH_CIDR:-0.0.0.0/0}" + +if ! command -v aws >/dev/null 2>&1; then + echo "ERROR: aws CLI not found. Install and configure it first." + exit 1 +fi + +if [[ -z "${KEY_PAIR_NAME}" ]]; then + echo "ERROR: KEY_PAIR_NAME is required." + echo "Example: KEY_PAIR_NAME=havenhold-app-key ./infra/create-instance.sh" + exit 1 +fi + +echo "[1/6] Creating instance ${INSTANCE_NAME} in ${AZ}..." +aws lightsail create-instances \ + --region "${REGION}" \ + --instance-names "${INSTANCE_NAME}" \ + --availability-zone "${AZ}" \ + --blueprint-id "${BLUEPRINT_ID}" \ + --bundle-id "${BUNDLE_ID}" \ + --key-pair-name "${KEY_PAIR_NAME}" >/dev/null + +echo "[2/6] Waiting for instance state=running..." +for _ in $(seq 1 60); do + state="$(aws lightsail get-instance-state \ + --region "${REGION}" \ + --instance-name "${INSTANCE_NAME}" \ + --query 'state.name' \ + --output text 2>/dev/null || true)" + if [[ "${state}" == "running" ]]; then + break + fi + sleep 5 +done + +if [[ "${state:-}" != "running" ]]; then + echo "ERROR: instance did not reach running state in time." + exit 1 +fi + +echo "[3/6] Allocating static IP ${STATIC_IP_NAME} (if needed)..." +if ! aws lightsail get-static-ip --region "${REGION}" --static-ip-name "${STATIC_IP_NAME}" >/dev/null 2>&1; then + aws lightsail allocate-static-ip \ + --region "${REGION}" \ + --static-ip-name "${STATIC_IP_NAME}" >/dev/null +fi + +echo "[4/6] Attaching static IP..." +aws lightsail attach-static-ip \ + --region "${REGION}" \ + --static-ip-name "${STATIC_IP_NAME}" \ + --instance-name "${INSTANCE_NAME}" >/dev/null + +echo "[5/6] Configuring public ports (Lightsail firewall)..." +# Use put-instance-public-ports so we enforce an exact allowlist and close any stale open ports. +aws lightsail put-instance-public-ports \ + --region "${REGION}" \ + --instance-name "${INSTANCE_NAME}" \ + --port-infos \ + "fromPort=${SSH_PORT},toPort=${SSH_PORT},protocol=TCP,cidrs=${SSH_CIDR}" \ + "fromPort=80,toPort=80,protocol=TCP,cidrs=0.0.0.0/0" \ + "fromPort=443,toPort=443,protocol=TCP,cidrs=0.0.0.0/0" >/dev/null + +STATIC_IP="$(aws lightsail get-static-ip \ + --region "${REGION}" \ + --static-ip-name "${STATIC_IP_NAME}" \ + --query 'staticIp.ipAddress' \ + --output text)" + +echo "[6/6] Complete" +echo "Instance: ${INSTANCE_NAME}" +echo "Static IP: ${STATIC_IP}" +echo "SSH port: ${SSH_PORT}" +echo + +echo "Next steps:" +echo "1) Copy provision script: scp -i ./infra/provision.sh ubuntu@${STATIC_IP}:/tmp/provision.sh" +echo "2) Run it: ssh -i -p ${SSH_PORT} ubuntu@${STATIC_IP} 'bash /tmp/provision.sh'" +echo "Tip: lock SSH source CIDR via SSH_CIDR=203.0.113.10/32 when possible." diff --git a/infra/provision.sh b/infra/provision.sh new file mode 100755 index 0000000..af77ead --- /dev/null +++ b/infra/provision.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +# H-005 one-time host baseline hardening script for Ubuntu Lightsail. +# Run as: bash /tmp/provision.sh (as ubuntu user with sudo access) + +DEPLOY_USER="${DEPLOY_USER:-havenadmin}" +SSH_PORT="${SSH_PORT:-22}" +ALLOW_UBUNTU_USER="${ALLOW_UBUNTU_USER:-true}" + +if ! command -v sudo >/dev/null 2>&1; then + echo "ERROR: sudo is required." + exit 1 +fi + +if [[ "$(id -u)" -ne 0 ]]; then + exec sudo -E bash "$0" "$@" +fi + +log() { + echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*" +} + +log "Step 1/7: Updating OS packages" +apt-get update -y +DEBIAN_FRONTEND=noninteractive apt-get full-upgrade -y + +log "Step 2/7: Installing security baseline packages" +DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades fail2ban ufw apt-listchanges + +log "Step 3/7: Enabling unattended upgrades" +cat >/etc/apt/apt.conf.d/20auto-upgrades <<'AUTOPATCH' +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; +APT::Periodic::AutocleanInterval "7"; +AUTOPATCH +systemctl enable unattended-upgrades >/dev/null 2>&1 || true +systemctl restart unattended-upgrades + +log "Step 4/7: Creating admin user ${DEPLOY_USER}" +if ! id -u "${DEPLOY_USER}" >/dev/null 2>&1; then + adduser --disabled-password --gecos "" "${DEPLOY_USER}" +fi +usermod -aG sudo "${DEPLOY_USER}" +log "NOTE: ${DEPLOY_USER} is key-only for SSH and has no local sudo password yet." +log "After this script, set one with: sudo passwd ${DEPLOY_USER}" + +install -d -m 700 -o "${DEPLOY_USER}" -g "${DEPLOY_USER}" "/home/${DEPLOY_USER}/.ssh" +if [[ -f /home/ubuntu/.ssh/authorized_keys ]]; then + cp /home/ubuntu/.ssh/authorized_keys "/home/${DEPLOY_USER}/.ssh/authorized_keys" + chown "${DEPLOY_USER}:${DEPLOY_USER}" "/home/${DEPLOY_USER}/.ssh/authorized_keys" + chmod 600 "/home/${DEPLOY_USER}/.ssh/authorized_keys" +else + log "WARNING: /home/ubuntu/.ssh/authorized_keys not found; add keys manually for ${DEPLOY_USER}." +fi + +log "Step 5/7: Hardening SSH config" +ALLOW_USERS="${DEPLOY_USER}" +if [[ "${ALLOW_UBUNTU_USER}" == "true" ]]; then + ALLOW_USERS="${ALLOW_USERS} ubuntu" +fi + +cat >/etc/ssh/sshd_config.d/99-havenhold.conf </etc/fail2ban/jail.d/sshd.conf </dev/null 2>&1 || true +systemctl restart fail2ban + +log "Hardening complete." +log "Validation commands:" +cat <