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.
79 lines
3.5 KiB
Markdown
79 lines
3.5 KiB
Markdown
# 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:**
|
|
```yaml
|
|
#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):**
|
|
```bash
|
|
# 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):**
|
|
```hcl
|
|
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.
|