Commit graph

18 commits

Author SHA1 Message Date
Claude
48b9ccf152
feat: M4 Hardening — encryption, resource limits, monitoring, backups
## Env var encryption at rest (AES-256-GCM)
- server/src/crypto.rs: new module — encrypt/decrypt with AES-256-GCM
  Key = SHA-256(HIY_SECRET_KEY); non-prefixed values pass through
  transparently for zero-downtime migration
- Cargo.toml: aes-gcm = "0.10"
- routes/envvars.rs: encrypt on SET; list returns masked values (••••)
- routes/databases.rs: pg_password and DATABASE_URL stored encrypted
- routes/ui.rs: decrypt pg_password when rendering DB card
- builder.rs: decrypt env vars when writing the .env file for containers
- .env.example: add HIY_SECRET_KEY entry

## Per-app resource limits
- apps table: memory_limit (default 512m) + cpu_limit (default 0.5)
  added via idempotent ALTER TABLE in db.rs migration
- models.rs: App, CreateApp, UpdateApp gain memory_limit + cpu_limit
- routes/apps.rs: persist limits on create, update via PUT
- builder.rs: pass MEMORY_LIMIT + CPU_LIMIT to build script
- builder/build.sh: use $MEMORY_LIMIT / $CPU_LIMIT in podman run
  (replaces hardcoded --cpus="0.5"; --memory now also set)

## Monitoring (opt-in compose profile)
- infra/docker-compose.yml: gatus + netdata under `monitoring` profile
  Enable: podman compose --profile monitoring up -d
  Gatus on :8080, Netdata on :19999
- infra/gatus.yml: Gatus config checking HIY /api/status every minute

## Backup cron job
- infra/backup.sh: dumps SQLite, copies env files + git repos into a
  dated .tar.gz; optional rclone upload; 30-day local retention
  Suggested cron: 0 3 * * * /path/to/infra/backup.sh

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-24 15:06:42 +00:00
Claude
8267b30b15
fix: restore app reachability after platform restart
Two root causes:

1. Caddy was started without --resume, so every restart wiped all
   dynamically-registered app routes (only the base Caddyfile survived).
   Adding --resume makes Caddy reload its auto-saved config (stored in
   the caddy-config volume) which includes all app routes.

2. App routes used the container IP address, which changes whenever
   hiy-net is torn down and recreated by compose. Switch to the
   container name as the upstream dial address; Podman's aardvark-dns
   resolves it by name within hiy-net, so it stays valid across
   network recreations.

Together with the existing reconnect loop in start.sh these two
changes mean deployed apps survive a platform restart without needing
a redeploy.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-24 12:47:32 +00:00
Claude
63a1ae6065
Remove --memory limit to avoid memory.swap.max cgroup error on Pi
Raspberry Pi OS does not enable swap cgroup accounting by default.
Even --memory-swap=-1 causes runc to write "max" to memory.swap.max,
which fails with ENOENT when the file does not exist.

Removing --memory entirely means runc skips all memory.* cgroup writes.
--cpus is unaffected (uses cpu.max, which is always present).

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-22 15:23:49 +00:00
Claude
ffe76144fb
Fix container start failure on Pi: disable cgroup swap limit
Raspberry Pi OS does not enable swap accounting in cgroups by default,
so the memory.swap.max cgroup v2 file does not exist.  Setting --memory
without --memory-swap causes runc to write a swap limit to that file,
which fails with ENOENT.

Adding --memory-swap=-1 tells runc to leave swap unlimited, skipping
the memory.swap.max write entirely.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-22 11:02:58 +00:00
Claude
4319b99102
Replace Docker with Podman throughout
- builder/build.sh: all docker commands → podman (build, run, stop, rm,
  network create, images, rmi, inspect)
- server/src/routes/apps.rs: docker stop/restart → podman
- server/src/routes/ui.rs: docker inspect → podman
- infra/Dockerfile.server: install podman instead of docker.io
- infra/docker-compose.yml: rename docker-proxy → podman-proxy, mount
  /run/podman/podman.sock (rootful Podman socket), update DOCKER_HOST
- infra/Makefile: docker compose → podman compose

Podman is daemonless and rootless by default; OCI images are identical so
no build-pipeline changes are needed beyond renaming the CLI.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-20 14:58:52 +00:00
Claude
2cdbf270f6
Add multi-user security service with per-app authorization
Control plane:
- Users and app grants stored in SQLite (users + user_apps tables)
- bcrypt password hashing
- Sessions: HashMap<token, user_id> (in-memory, cleared on restart)
- Bootstrap: first admin auto-created from HIY_ADMIN_USER/HIY_ADMIN_PASS if DB is empty
- /admin/users page: create/delete users, toggle admin, grant/revoke app access
- /api/users + /api/users/:id/apps/:app_id REST endpoints (admin-only)

Deployed apps:
- Every app route now uses Caddy forward_auth pointing at /auth/verify
- /auth/verify checks session cookie + user_apps grant (admins have access to all apps)
- Unauthenticated -> 302 to /login?next=<original URL>
- Authorised but not granted -> /denied page
- Session cookie set with Domain=.DOMAIN_SUFFIX for cross-subdomain auth

Other:
- /denied page for "logged in but not granted" case
- Login page skips re-auth if already logged in
- Cookie uses SameSite=Lax (required for cross-subdomain redirect flows)

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-20 14:22:57 +00:00
Claude
6ff8c9a267
Fix Caddy route registration: discover server name dynamically
Caddy's Caddyfile adapter names servers 'srv0' (not 'hiy'), so
PATCHing /config/apps/http/servers/hiy/routes was a no-op. Now we
query /config/apps/http/servers/ to find the actual server name
before updating routes.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-20 13:34:21 +00:00
Claude
944feb39ec
fix: pass PORT env var to app container
Apps follow the Heroku convention of binding to $PORT at runtime.
Without --env PORT=$PORT, containers use their default port which
doesn't match what Caddy is configured to dial, causing 502s.
2026-03-19 11:34:33 +00:00
Claude
d7eb5ef6fe
fix: use PATCH not PUT to update Caddy routes
Caddy admin API: PUT creates a key (409 if exists), PATCH replaces it.
Since routes always exists after startup from caddy.json, PATCH is correct.
2026-03-19 11:28:00 +00:00
Claude
2df3c579e4
fix: switch Docker access to TCP via socat proxy; add Caddy error logging
- Add docker-proxy (alpine/socat) sidecar that exposes the Docker Unix
  socket as TCP on port 2375, so server needs no privileged socket mount
- Set DOCKER_HOST=tcp://docker-proxy:2375 in server environment
- App containers are still spawned on the host daemon and join hiy-net,
  so Caddy can still reach them
- Log actual Caddy PUT response body and HTTP status on failure
  instead of a silent warning
2026-03-19 11:24:50 +00:00
Claude
2e98ce957e
fix: make Caddy route upsert robust against missing/invalid routes
- Add --fail to the GET so a 404 (no 'hiy' server yet, stale volume)
  falls back to [] instead of passing error JSON to Python
- Python now guards against non-list responses with try/except
- Always re-append the dashboard catch-all route so it survives
  even when routes are rebuilt from scratch
2026-03-19 11:17:06 +00:00
Claude
a8b22d8e2d
fix: switch to Caddy JSON config so dynamic routes work correctly
The Caddyfile created a server with an auto-generated name, not 'hiy',
so build.sh's PUT to /config/apps/http/servers/hiy/routes was creating
a parallel server that never received traffic.

- Replace Caddyfile with caddy.json that names the server 'hiy' with
  the dashboard as a catch-all fallback route
- Insert app routes at index 0 so host-matched routes are evaluated
  before the catch-all dashboard fallback
- Update docker-compose to mount caddy.json and pass --config flag
2026-03-19 11:02:57 +00:00
Claude
11db59d612
fix: guard against non-dict route entries when filtering Caddy routes
r.get() crashed when the Caddy API returned a routes array containing
string elements. Added isinstance(r, dict) check and also made the
match[0] traversal safer by using any() over the match list.
2026-03-19 10:55:40 +00:00
Claude
3d35e220e4
build.sh: give actionable instructions when Caddy is unreachable
The old message just said 'expose manually if needed' with no guidance.
Now it prints the exact docker commands to publish the port directly
and how to find the container IP for a custom reverse proxy.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-19 10:27:17 +00:00
Claude
54ddc0d856
build.sh: fix empty array expansion under set -u
${arr[@]} with set -u throws 'unbound variable' when the array is empty.
Use ${arr[@]+"${arr[@]}"} which only expands if the array is set.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-19 09:58:20 +00:00
Claude
efca2b5941
build.sh: skip --env-file when env file does not exist
docker run fails hard if the path passed to --env-file is missing.
Make the flag conditional so apps without an env file (or where the
file hasn't been created yet) still start successfully.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-19 09:47:21 +00:00
Shautvast
98385ea743 proper var use 2026-03-19 10:45:52 +01:00
Claude
8f5bb158cb
M1: Rust control plane, builder, dashboard, and infra
- Cargo workspace with hiy-server (axum 0.7 + sqlx SQLite + tokio)
- SQLite schema: apps, deploys, env_vars (inline migrations, no daemon)
- Background build worker: sequential queue, streams stdout/stderr to DB
- REST API: CRUD for apps, deploys, env vars; GitHub webhook with HMAC-SHA256
- SSE endpoint for live build log streaming
- Monospace HTMX-free dashboard: app list + per-app detail, log viewer, env editor
- builder/build.sh: clone/pull → detect strategy (Dockerfile/buildpack/static)
  → docker build → swap container → update Caddy via admin API → prune images
- infra/docker-compose.yml + Dockerfile.server for local dev (no Pi needed)
- proxy/Caddyfile: auto-HTTPS off for local, comment removed for production
- .env.example

Compiles clean (zero warnings). Run locally:
  cp .env.example .env && cargo run --bin hiy-server

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-19 08:25:59 +00:00