vps-0836dabe.vps.ovh.caStatus at 2026-11-06 00:38 UTC: DEPLOYED and VERIFIED, with access by IP.
- Active release: 20261106T003554Z.
- healthcheck.sh: health=OK warn=[] crit=[].
- Not available yet: a public HTTPS certificate. There is no domain, so the IP access uses an internal-CA certificate (§5).
| Host | vps-0836dabe.vps.ovh.ca · 158.69.190.114 · 2607:5300:205:200::fcff |
| System (verified) | Ubuntu 24.04.4 LTS, kernel 6.8.0-136, 4 vCPU, 7.6 GiB RAM, 72 GB disk + 2 GB swap (added) |
| SSH host key (after reinstall) | ED25519 SHA256:9czS8hdsSH5g8YYhATWqtUQar5dkTZKCyC8/XUPbjl4 |
| Deploy key | ~/.ssh/auction_oracle_ovh (ed25519) on the owner's PC only. Server-side fingerprint: SHA256:21m5+r3H…6RBg. |
The machine was blank:
- no Docker, no existing services;
- only our key in authorized_keys;
- passwordless sudo for ubuntu;
- 0 pending updates.
To fix:
- UFW was inactive.
- SSH accepted passwords (50-cloud-init.conf) and root by key.
- There was no swap.
Report saved at /root/ao-preflight-20261106T001314Z.txt.
Internet ─► Caddy 2.8.4 :80 → 308 → :443 (https://158.69.190.114, internal-CA certificate)
├─ /status, /api/health, /docs/*, /site/* public
└─ everything else ─► api (HTTP basic auth)
SSH tunnel ─► 127.0.0.1:8765 ─► api (FastAPI)
worker (collectors, hourly) · scheduler (jobs) · monitor (metrics 60 s)
└──► PostgreSQL 16.15 (internal network only, no published port)
backend is internal (Postgres plus the app services);edge connects Caddy and the API;egress gives internet access to the collectors only.| Component | Version | Memory used / limit |
|---|---|---|
| Docker Engine / Compose | 29.9.3 / 5.7.0 | — |
| PostgreSQL | 16.15 (alpine) | 85 MB / 2 GB |
| Caddy | 2.8.4 | 12 MB / 256 MB |
| api | Python 3.12, app 0.1.0 | 88 MB / 1.2 GB |
| worker | same | 108 MB / 1.5 GB |
| scheduler | same | 30 MB / 1.5 GB |
| monitor | same | 35 MB / 160 MB |
| Measure | Result |
|---|---|
| SSH | permitrootlogin no, passwordauthentication no, kbdinteractiveauthentication no, allowusers ubuntu, MaxAuthTries 4. Tested from outside: ubuntu and root with a password both return Permission denied (publickey). |
| Lockout protection | lockdown applied with a 5-minute automatic revert, confirmed from a new key session before the revert was cancelled |
| UFW | active, default deny incoming. Open: 22/tcp (rate-limited), 80/tcp, 443/tcp, 443/udp (IPv4 + IPv6). |
| Public listeners | only :22, :80, :443. PostgreSQL and the API (127.0.0.1) are not exposed. |
| fail2ban | sshd + recidive jails. First attacker banned automatically (203.123.32.230) a few minutes after the server came online. |
| Updates | unattended-upgrades for security updates, no automatic reboot (health check warns when one is needed). Reboot done and verified. |
| Kernel / logs | sysctl hardening, persistent journald (500 MB / 3 months), ops logs rotated weekly |
| Secrets | generated on the server, /opt/auction-oracle/shared/* mode 600 root. Never printed, never in Git. |
| Database | roles ao_owner (migrations), ao_app (DML only), ao_readonly. SCRAM-SHA-256. Separate test database. |
| App access | no credentials → 401 on / and /api/*. Correct credentials → 200. Wrong password → 401. Tested on the server without ever displaying the password. |
| Containers | app images run as a non-root user with a read-only filesystem and no-new-privileges. Logs rotate (10 MB × 5). |
Earlier hardening had backed up every modified configuration file to /root/ao-config-backup/<ts>/.
| What | URL |
|---|---|
| Application (login required) | https://158.69.190.114/ |
| Public status | https://158.69.190.114/status |
| Documentation | https://158.69.190.114/docs/ |
| JSON health | https://158.69.190.114/api/health |
oracle. The password is stored root-only on the server; read it with ssh … "sudo cat /opt/auction-oracle/shared/app-access.txt", then save it in your password manager..\ao-remote.ps1 tunnel, then http://127.0.0.1:8765.When you get a domain: point its A/AAAA records to the VPS and run update.sh --caddy (or deploy again). The auctionoracle.com / app. / api. / docs. / status. blocks switch on automatically, with Let's Encrypt, once DNS resolves to the server.
| Test | Result |
|---|---|
| Backup | ao-20261106T002038Z.dump: 464 KB, 24 tables, checked with pg_restore -l and sha256, plus a config/secrets archive |
| Restore test | restored into ao_restore_test: items=10639, observations=8553, recipes=3908, schema v2. Identical to production. Then dropped. |
| Off-site | encrypted with age (age1kct477f2…ea2s49). The private key lives only on the PC (%USERPROFILE%\.auction-oracle\backup-identity.txt). |
| Pull to PC + decrypt | 2 files pulled to %USERPROFILE%\AuctionOracleBackups, 0 checksum errors. The decrypted dump is byte-for-byte identical to the server dump (same SHA-256). |
| Automation | ao-backup.timer daily at 03:30 UTC, ao-restore-test.timer every Sunday at 05:00 UTC, plus a backup before every deploy and every restore (seen in each deploy) |
Your action:
- Keep a copy of backup-identity.txt in a password manager. Without it, the off-site backups can't be decrypted.
- The daily pull to the PC is still manual (.\ao-remote.ps1 pull-backups). Creating a Windows scheduled task needs your approval.
| Test | Result |
|---|---|
| First deploy | build, migrations (owner role) + grants, 32/32 tests on PostgreSQL, 6 services healthy |
| Reboot | all 6 containers came back on their own, health=OK, collector ran again |
| Second deploy | pre-deploy backup, 32/32 tests, deploy OK |
| Rollback | rollback.sh: API on the previous release image, health OK. Roll-forward also OK. |
| Third deploy (fixes) | backup, 32/32 tests, health=OK warn=[] crit=[] |
Deployments run as transient systemd units (run-detached.sh), so an SSH disconnect can't interrupt them.
monitor service: CPU, RAM, swap, disk, network, DB size and connections, and API latency every 60 s, stored in the metrics table (30 days).healthcheck.sh every 5 minutes writes to /var/log/auction-oracle/health.log and syslog. It checks:ok on first run.| Criterion | Status |
|---|---|
| SSH key login works | ✅ |
| Server secured | ✅ (UFW, fail2ban, SSH key-only / no root, updates, sysctl) |
| Docker works | ✅ 29.9.3 |
| Services start automatically | ✅ checked after a reboot |
| PostgreSQL operational | ✅ 16.15, least-privilege roles |
| API responds | ✅ |
| HTTPS site after DNS | ⏳ no domain yet. HTTPS by IP works (internal-CA certificate). |
| Collectors work | ✅ auctionhouse.fyi + Everloot, fresh data |
| Scheduled jobs work | ✅ |
| Backups work | ✅ |
| Restore tested | ✅ identical counts |
| Resources monitored | ✅ |
| Logs usable | ✅ (docker json-file rotated, journald, /var/log/auction-oracle/*.log) |
| Rollback works | ✅ |
ipv6 setting).shared/app.env on the server. These sources are disabled until then.From the PC (deploy\windows):
.\ao-remote.ps1 health # health report + container usage
.\ao-remote.ps1 deploy # new release (backup, tests on PostgreSQL, rollout, auto-rollback)
.\ao-remote.ps1 rollback # previous release
.\ao-remote.ps1 backup # backup now
.\ao-remote.ps1 restore-test # restore test now
.\ao-remote.ps1 pull-backups # copy encrypted backups to the PC
.\ao-remote.ps1 tunnel # app at http://127.0.0.1:8765 over SSH
On the server:
sudo /opt/auction-oracle/bin/healthcheck.sh
sudo /opt/auction-oracle/bin/update.sh --system # OS updates
sudo /opt/auction-oracle/bin/update.sh --caddy # after pointing a domain
sudo /opt/auction-oracle/bin/restore.sh --file /opt/auction-oracle/backups/db/<dump> --confirm
sudo tail -f /var/log/auction-oracle/ops.log /var/log/auction-oracle/health.log
sudo cat /opt/auction-oracle/shared/app-access.txt