KeiSeiKit-1.0/_blocks/deploy-vps-generic.md
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

3.5 KiB

DEPLOY — Generic VPS (provider-agnostic cloud-init + ssh-first-contact)

Target providers: DigitalOcean Droplets, Vultr, UpCloud, Linode/Akamai. Each has slightly different Terraform providers + CLIs, but the Day-0 contract is identical: boot a Debian/Ubuntu image with a cloud-init user-data blob; add one admin SSH key; nothing else.

Day-0 cloud-init blob (cloud-init.yaml) — universal:

#cloud-config
hostname: kei-${env}-${role}
timezone: UTC
package_update: true
package_upgrade: true
packages:
  - ufw
  - fail2ban
  - unattended-upgrades
  - auditd
  - needrestart
  - curl
  - jq
users:
  - name: keiadmin
    groups: sudo
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh_authorized_keys:
      - ${ADMIN_PUBKEY}
ssh_pwauth: false
disable_root: true
write_files:
  - path: /etc/ssh/sshd_config.d/99-kei.conf
    permissions: '0644'
    content: |
      PasswordAuthentication no
      PermitRootLogin no
      MaxAuthTries 3
      AllowUsers keiadmin
      ClientAliveInterval 120
      ClientAliveCountMax 2      
runcmd:
  - [ systemctl, restart, ssh ]
  - [ ufw, default, deny,  incoming ]
  - [ ufw, default, allow, outgoing ]
  - [ ufw, allow,   22/tcp ]
  - [ ufw, --force, enable ]

The blob is intentionally provider-neutral. Provider-specific bits (private-network bring-up, metadata service quirks) go in a short appendix the provisioner appends. See _primitives/harden-base.sh for post-boot hardening re-runs.

SSH-first-contact (ssh-first-contact.sh pattern):

# Wait for cloud-init to finish AND sshd to be ready on the new IP.
for i in $(seq 1 60); do
  ssh -o ConnectTimeout=3 -o StrictHostKeyChecking=accept-new \
      "keiadmin@$IP" "cloud-init status --wait" && break
  sleep 5
done
ssh "keiadmin@$IP" "sudo test -f /var/lib/cloud/instance/boot-finished"

StrictHostKeyChecking=accept-new is OK only for the FIRST contact (TOFU). Store the fingerprint to ~/.ssh/known_hosts; subsequent connects use default strict mode. Never use StrictHostKeyChecking=no — accepts MitM silently.

Terraform skeleton (provider-agnostic via vars):

variable "provider_kind" {}                   # "digitalocean" | "vultr" | "upcloud" | "linode"
variable "region"       {}
variable "size_slug"    {}                    # provider-specific size id
variable "admin_pubkey" {}                    # raw ssh-ed25519 …
locals {
  user_data = templatefile("${path.module}/cloud-init.yaml", { ADMIN_PUBKEY = var.admin_pubkey })
}
# ... then a module-per-provider resource that all read `local.user_data`

Keep TF state local per-env-per-dev by default; upgrade to remote backend (R2, S3, Terraform Cloud) only when ≥ 2 humans share state.

Per-provider gotchas (verified 2026-04-21):

  • DigitalOcean: Marketplace "Docker" images skip unattended-upgrades — start from plain Debian 12 instead. IPv6 requires ipv6 = true on the droplet.
  • Vultr: vultr-cli needs VULTR_API_KEY; default firewall is OPEN — attach a firewall group or rely solely on ufw.
  • UpCloud: IPs rotate on full stop+start unless you request floating_ip. Consider Finnish ASN if Hetzner is blocked or rate-limited for your geo.
  • Linode: cloud-init runs before disk resize on some plans → growpart may need a rerun on first ssh.

Forbidden: baking the admin private key into an AMI/snapshot; reusing one SSH keypair across envs; letting cloud-init pull scripts from a mutable URL (curl … | bash in runcmd: — pin to a hash); running apt-get dist-upgrade -y in runcmd without needrestart to surface pending reboots.