Mandatory rules for all new code based on 33 pentest findings. Covers: input validation, auth checks, SSRF prevention, session management, CSP, nginx config, container security, RBAC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
404 lines
17 KiB
Markdown
404 lines
17 KiB
Markdown
# CLAUDE.md — Archipelago (Archy) Project Guide
|
|
|
|
## Project Overview
|
|
|
|
Archipelago is a **Bitcoin Node OS** — a bootable, self-sovereign personal server you flash to USB, install on hardware, and manage via a web UI. Similar to Umbrel/Start9/RaspiBlitz but custom-built with production-grade security.
|
|
|
|
**Stack**: Rust backend + Vue 3 (Composition API) + TypeScript (strict) + Vite 7 + Tailwind CSS + Pinia + Podman
|
|
**Target OS**: Debian 12 (Bookworm) — x86_64 and ARM64
|
|
**Current version**: 0.1.0
|
|
|
|
---
|
|
|
|
## BETA FREEZE — ACTIVE (2026-03-18)
|
|
|
|
**Goal: Ship a flawless beta that works perfectly on every machine we install it on.**
|
|
|
|
We are in **beta stabilization mode**. The current feature set is LOCKED. Every session must push toward this goal.
|
|
|
|
### Pipeline
|
|
|
|
```
|
|
PHASE 1: Feature Testing (internal) ← WE ARE HERE
|
|
↓ Gate: every feature works, bugs fixed, security hardened, ISO verified
|
|
PHASE 2: User Testing (real users on real hardware we don't control)
|
|
↓ Gate: user-reported issues resolved, telemetry shows stable fleet
|
|
PHASE 3: Beta Live (public release)
|
|
```
|
|
|
|
### What IS allowed
|
|
- Bug fixes for existing features
|
|
- Security hardening and testing
|
|
- Beta telemetry / node reporting (TASK-12 — needed for user testing)
|
|
- UI/layout rearrangements (moving things around, improving flow)
|
|
- Boot screen completion (FEATURE-4 — already in progress)
|
|
- Testing all features end-to-end on fresh installs
|
|
- Performance and reliability improvements to existing code
|
|
- ISO build hardening
|
|
|
|
### What is NOT allowed
|
|
- New features (watch-only wallet, mesh balance check, etc. are POST-BETA)
|
|
- New app integrations
|
|
- New backend modules or RPC endpoints (unless fixing existing bugs or beta telemetry)
|
|
- New dependencies (unless required for beta infrastructure)
|
|
- Scope creep of any kind
|
|
|
|
### Status tracking
|
|
- **Progress tracker**: `docs/BETA-PROGRESS.md` — updated every session
|
|
- **Beta checklist**: `docs/BETA-RELEASE-CHECKLIST.md` — the acceptance criteria
|
|
- **Master plan**: `docs/MASTER_PLAN.md` — phased roadmap (Phase 1/2/3)
|
|
|
|
### Session protocol
|
|
1. Read `docs/BETA-PROGRESS.md` at start of every session
|
|
2. Report current phase and status before starting work
|
|
3. Work only on current-phase items
|
|
4. Update `docs/BETA-PROGRESS.md` at end of every session with what changed
|
|
|
|
---
|
|
|
|
## Quick Reference
|
|
|
|
```bash
|
|
# Frontend local dev (mock backend on :5959, Vite on :8100)
|
|
cd neode-ui && npm start
|
|
|
|
# Deploy to live server (frontend + backend + restart services)
|
|
./scripts/deploy-to-target.sh --live
|
|
|
|
# Deploy to both servers
|
|
./scripts/deploy-to-target.sh --both
|
|
|
|
# Frontend build (outputs to web/dist/neode-ui/)
|
|
cd neode-ui && npm run build
|
|
|
|
# Type-check frontend
|
|
cd neode-ui && npm run type-check
|
|
|
|
# Rust checks (run on dev server, NOT macOS)
|
|
cargo clippy --all-targets --all-features
|
|
cargo fmt --all
|
|
cargo test --all-features
|
|
```
|
|
|
|
Dev server: `http://192.168.1.228` | Local frontend: `http://localhost:8100` (password: `password123`)
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Debian 12 (Bookworm)
|
|
├── Podman (rootless containers)
|
|
├── Nginx (port 80 → proxies /rpc/, /ws/, /health to backend)
|
|
├── Rust Backend (core/) — binary on port 5678
|
|
│ ├── core/archipelago/ — Main binary, RPC endpoints
|
|
│ ├── core/container/ — PodmanClient, manifest parser, dependency resolver, health monitor
|
|
│ ├── core/security/ — AppArmor profiles, secrets manager, Cosign image verifier
|
|
│ ├── core/performance/ — Resource manager
|
|
│ └── core/parmanode/ — Parmanode compatibility layer
|
|
└── Vue.js UI (neode-ui/)
|
|
├── src/api/ — RPC client (rpc-client.ts), WebSocket, container client
|
|
├── src/stores/ — Pinia stores
|
|
├── src/views/ — Page components
|
|
├── src/components/ — Reusable components
|
|
├── src/router/ — Vue Router
|
|
├── src/types/ — TypeScript type definitions
|
|
└── src/style.css — Global styles + Tailwind utilities
|
|
```
|
|
|
|
### Data Paths (Server)
|
|
|
|
- App data: `/var/lib/archipelago/{app-id}/`
|
|
- Secrets: `/var/lib/archipelago/secrets/{app-id}/` (encrypted)
|
|
- Frontend: `/opt/archipelago/web-ui/`
|
|
- Backend binary: `/usr/local/bin/archipelago`
|
|
- Systemd service: `/etc/systemd/system/archipelago.service`
|
|
- Nginx config: `/etc/nginx/sites-available/archipelago`
|
|
|
|
## CRITICAL Workflow Rules
|
|
|
|
### 1. NEVER Build Rust on macOS for Linux
|
|
|
|
Always rsync source to the Linux dev server and build there. Building on macOS and copying the binary causes Exec format errors.
|
|
|
|
```bash
|
|
# Deploy does this automatically:
|
|
./scripts/deploy-to-target.sh --live
|
|
```
|
|
|
|
### 2. Always Deploy After Changes
|
|
|
|
After editing code (frontend, backend, scripts, or configs), deploy to the live server. Do not leave deployment to the user.
|
|
|
|
### 3. Frontend Build Output Path
|
|
|
|
Frontend builds to `web/dist/neode-ui/` — NOT `neode-ui/dist/`.
|
|
|
|
### 4. Deploy-Test-Fix Loop
|
|
|
|
1. Make the change
|
|
2. Deploy with `./scripts/deploy-to-target.sh --live`
|
|
3. Test at http://192.168.1.228
|
|
4. If broken, fix and redeploy — repeat until working
|
|
5. End loop only when everything works
|
|
|
|
### 5. SSH Access
|
|
|
|
- **Primary**: `archipelago@192.168.1.228` — password: `EwPDR8q45l0Upx@`
|
|
- **Secondary**: `archipelago@192.168.1.198`
|
|
- Credentials stored in gitignored `scripts/deploy-config.sh`
|
|
|
|
```bash
|
|
sshpass -p 'EwPDR8q45l0Upx@' ssh -o StrictHostKeyChecking=no archipelago@192.168.1.228
|
|
```
|
|
|
|
## Frontend Rules (Vue.js + TypeScript)
|
|
|
|
### Component Standards
|
|
|
|
- **Always** `<script setup lang="ts">` — never Options API, never plain JS
|
|
- **Pinia** for all state management — focused single-purpose stores
|
|
- **TypeScript strict mode** — no `any`, use `unknown` or proper types
|
|
- Export types from dedicated `.types.ts` files
|
|
- Use type guards for runtime type checking
|
|
|
|
### Styling — Global Classes Only
|
|
|
|
- **ALWAYS** create global utility classes in `neode-ui/src/style.css`
|
|
- **NEVER** use inline Tailwind classes directly in components
|
|
- Use semantic class names: `.glass-card`, `.glass-button`, `.gradient-button`, `.path-option-card`
|
|
|
|
### API Client Rules
|
|
|
|
- Use `@/api/rpc-client.ts` for RPC calls, `@/api/container-client.ts` for containers
|
|
- **NEVER** hardcode API endpoints — use environment variables
|
|
- Handle loading states, error states, retry logic for all async operations
|
|
|
|
### CSS Class Hierarchy
|
|
|
|
| Class | Use | Hover |
|
|
|-------|-----|-------|
|
|
| `.path-option-card` | Section containers, interactive cards (Settings-style) | Lifts -2px |
|
|
| `.glass-card` | Content containers, modals, panels | No |
|
|
| `.info-card` | Status badges, metric displays | No |
|
|
| `.info-card-button` | Action buttons inside info sections | Lifts, brightens |
|
|
| `bg-black/20 rounded-xl border border-white/10` | Info sub-cards inside sections | No |
|
|
| `bg-white/5` | Simple read-only info rows | No |
|
|
| `.glass-button` | ALL buttons (primary and secondary) | Subtle brighten |
|
|
| `.path-action-button` | Large action buttons (Logout, Continue) | Lifts -2px |
|
|
|
|
### BANNED Classes — Do NOT Use
|
|
- **`.gradient-button`** — REMOVED. Use `.glass-button` instead. The gradient style breaks the clean glass aesthetic.
|
|
- **`.gradient-card`** / **`.gradient-card-dark`** — REMOVED. Use `.glass-card` or `.path-option-card` instead.
|
|
|
|
### Design Tokens
|
|
|
|
- **Font**: Avenir Next (primary), Montserrat (`font-archipelago`)
|
|
- **Spacing**: 4px grid system, 16px default padding
|
|
- **Glassmorphism**: `background: rgba(0,0,0,0.60)`, `backdrop-filter: blur(24px)`, `inset 0 1px 0 rgba(255,255,255,0.22)`
|
|
- **Transitions**: `all 0.3s ease` standard, `translateY(-2px)` hover, `translateY(1px)` active
|
|
- **Accent orange** (Bitcoin): `#fb923c` — `#f59e0b`
|
|
- **Green** (success): `#4ade80` | **Red** (danger): `#ef4444` | **Blue** (info): `#3b82f6`
|
|
- **Text**: `rgba(255,255,255,0.9)` primary, `rgba(255,255,255,0.6-0.7)` muted
|
|
|
|
### Tailwind Custom Values
|
|
|
|
- Blur: `backdrop-blur-glass` (18px), `backdrop-blur-glass-strong` (24px)
|
|
- Colors: `glass-dark` (0,0,0,0.35), `glass-darker` (0,0,0,0.6), `glass-border` (255,255,255,0.18)
|
|
- Shadows: `shadow-glass`, `shadow-glass-inset`
|
|
|
|
## Backend Rules (Rust)
|
|
|
|
### Error Handling
|
|
|
|
- **No `unwrap()` or `expect()` in production code** — use `?` operator
|
|
- `thiserror` for library error types, `anyhow` for application errors
|
|
- Custom error types per module: `{module}::Error`
|
|
- Include context: `.context("What failed and why")`
|
|
|
|
### RPC Endpoints
|
|
|
|
- Use `rpc_toolkit::command` macro for all endpoints
|
|
- Use `#[context] ctx: RpcContext` for context
|
|
- Return `Result<T, Error>` — validate all inputs before processing
|
|
|
|
### Async & Runtime
|
|
|
|
- `tokio` runtime only — never mix with other async runtimes
|
|
- Set timeouts on all external operations
|
|
- Use `select!` for racing futures with timeouts
|
|
- Handle shutdown gracefully with cancellation tokens
|
|
|
|
### Code Organization
|
|
|
|
- New modules in `core/{module-name}/`, add to `core/Cargo.toml` members
|
|
- `snake_case` for all modules/files
|
|
- Run `cargo clippy --all-targets --all-features` and `cargo fmt --all` before commits
|
|
|
|
### Logging
|
|
|
|
- Use `tracing` for structured logging — never `println!`
|
|
- Never log secrets, passwords, keys, or tokens
|
|
- Include context: `tracing::info!(user_id = %id, "Action")`
|
|
|
|
## Container & Security
|
|
|
|
### App Manifests
|
|
|
|
- All manifests in `apps/{app-id}/manifest.yml`
|
|
- Follow spec in `docs/app-manifest-spec.md`
|
|
- Use `archipelago_container::PodmanClient` — **NEVER** call Docker directly
|
|
|
|
### Security Requirements (Non-Negotiable)
|
|
|
|
- **ALWAYS** `readonly_root: true` unless explicitly needed
|
|
- **ALWAYS** drop all capabilities, add only required ones
|
|
- **ALWAYS** run as non-root user (UID > 1000)
|
|
- **ALWAYS** `no-new-privileges: true`
|
|
- **NEVER** use `latest` tag — pin specific image versions
|
|
- **NEVER** hardcode secrets — use `core/security/secrets_manager.rs`
|
|
|
|
### App Icons
|
|
|
|
Single source of truth: `neode-ui/public/assets/img/app-icons/`
|
|
Naming: `{app-id}.{png|webp|svg}` — do not duplicate elsewhere.
|
|
|
|
## Security Standards (Post-Pentest — Mandatory)
|
|
|
|
These rules come from a full penetration test (33 findings, all remediated). Follow them for ALL new code.
|
|
|
|
### Backend (Rust)
|
|
|
|
- **Backend binds to 127.0.0.1 ONLY** — never `0.0.0.0`. All external access goes through nginx.
|
|
- **Validate ALL user input before path construction** — reject `..`, `/`, `\`, null bytes. Use the existing `validate_app_id()` pattern in `tor.rs`.
|
|
- **Never pass user input to `sudo` commands** — if unavoidable, validate strictly against an allowlist of characters `[a-zA-Z0-9_-]`.
|
|
- **Every HTTP endpoint that returns sensitive data MUST check authentication** — use `self.is_authenticated(&headers).await` or be in `UNAUTHENTICATED_METHODS` with justification.
|
|
- **Rate-limit authentication endpoints** — `extract_client_ip()` must only trust `X-Real-IP` from the loopback interface (127.0.0.1).
|
|
- **Federation messages require ed25519 signatures** — never accept unsigned peer-joined messages.
|
|
- **RBAC: use explicit allowlists, not prefix matching** — `method.starts_with("node.")` is BANNED. List exact methods per role.
|
|
- **Session cookies: `SameSite=Lax; HttpOnly; Path=/`** — `Strict` breaks iframe app fetches. `Lax` still prevents CSRF on POST.
|
|
- **Destructive operations require password re-verification** — factory reset, onboarding reset, identity export.
|
|
- **Remember-me secrets: use `OsRng` random bytes** — never derive from `/etc/machine-id` or other public data.
|
|
- **Rotate session tokens after privilege escalation** — TOTP verification must issue a new token, invalidating the pending one.
|
|
- **Tar archive extraction: validate every entry path** — never use `archive.unpack()`. Iterate entries and verify no `..` components or paths escaping the target directory.
|
|
|
|
### Frontend (Vue/TypeScript)
|
|
|
|
- **Validate redirect URLs** — use `isLocalRedirect()` from `router/index.ts` before any `window.location.href` assignment. Reject `javascript:`, protocol-relative (`//`), and external URLs.
|
|
- **Never use `v-html` with user input** — if unavoidable, always sanitize with `DOMPurify.sanitize()`.
|
|
- **CSP: no `unsafe-inline` in `script-src`** — Vite builds don't need it. Keep `unsafe-inline` only in `style-src` for Tailwind.
|
|
|
|
### Nginx
|
|
|
|
- **Session validation: `$cookie_session` (not `$cookie_session_id`)** — cookie name must match the Rust backend's `session=` cookie.
|
|
- **Prefer `auth_request` over cookie-presence checks** — `if ($cookie_session = "")` only checks presence, not validity. For sensitive endpoints, use nginx `auth_request` to validate against the backend.
|
|
- **All `/app/*` proxies are unauthenticated at nginx level** — each app must handle its own auth. Never expose apps with default credentials (change Grafana `admin/admin` on first boot, etc.).
|
|
|
|
### SSRF Prevention
|
|
|
|
- **Validate all user-supplied URLs** — require `https://` scheme, reject private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7).
|
|
- **Disable redirect following** — use `redirect(Policy::none())` on reqwest clients that fetch user-supplied URLs.
|
|
- **Onion addresses: validate v3 format** — exactly 56 base32 `[a-z2-7]` chars + `.onion`.
|
|
- **Webhook URLs: parse with `Url::parse`** — don't split on `:` for host extraction (breaks IPv6).
|
|
|
|
### Container Security
|
|
|
|
- **Memory limits on every container** — use `--memory=$(mem_limit <name>)` pattern from `first-boot-containers.sh`. Prevents one container from OOM-killing the system.
|
|
- **Health checks on every container** — define via `--health-cmd` in `podman run`.
|
|
- **User-stopped tracking** — when a user stops a container via UI, record in `user-stopped.json` so crash recovery and health monitor don't auto-restart it.
|
|
|
|
## Code Quality
|
|
|
|
- Zero compiler warnings (Rust and TypeScript)
|
|
- Zero linter errors (clippy, eslint)
|
|
- Functions under 50 lines, single responsibility
|
|
- Comment WHY not WHAT — code should be self-documenting
|
|
- Remove dead code entirely — never comment it out
|
|
- No `TODO`/`FIXME` in commits — fix now or create issues
|
|
- Workspace-relative paths only — **NEVER** hardcode `/Users/dorian/...`
|
|
|
|
## Git Conventions
|
|
|
|
### Commit Format
|
|
|
|
```
|
|
type: description
|
|
```
|
|
|
|
**Types**: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `perf:`
|
|
|
|
### Rules
|
|
|
|
- Atomic commits — one logical change per commit
|
|
- `main` branch always production-ready
|
|
- Feature branches: `feature/description`, bug fixes: `fix/description`
|
|
- Never commit secrets, `.env` files, or credentials
|
|
- Tag releases: `v1.2.3` (SemVer)
|
|
|
|
## App Integration Checklist
|
|
|
|
When adding or fixing apps, **every file below must be checked**. Missing any one causes failures on fresh installs.
|
|
|
|
### Backend (Rust)
|
|
|
|
- [ ] `core/archipelago/src/api/rpc/package.rs` — `get_app_config()`: ports, volumes, env vars, custom args
|
|
- [ ] `core/archipelago/src/api/rpc/package.rs` — `needs_archy_net`: add if app needs container DNS
|
|
- [ ] `core/archipelago/src/api/rpc/package.rs` — `get_app_capabilities()`: add required caps (CHOWN, etc.)
|
|
- [ ] `core/archipelago/src/api/rpc/package.rs` — dependency checks (e.g., electrs requires bitcoin)
|
|
- [ ] `core/archipelago/src/container/docker_packages.rs` — `get_app_metadata()`: title, description, icon, repo
|
|
- [ ] `core/archipelago/src/container/docker_packages.rs` — UI address mapping (e.g., `http://localhost:50002`)
|
|
|
|
### Frontend (Vue)
|
|
|
|
- [ ] `neode-ui/src/views/Marketplace.vue` — `getCuratedAppList()`: marketplace entry with dockerImage
|
|
- [ ] `neode-ui/src/stores/appLauncher.ts` — port-to-proxy mapping (if app has custom UI port)
|
|
- [ ] `neode-ui/src/views/AppDetails.vue` — route ID mapping (if app ID differs from container name)
|
|
|
|
### Nginx
|
|
|
|
- [ ] `image-recipe/configs/nginx-archipelago.conf` — `/app/{id}/` proxy in HTTP block
|
|
- [ ] `image-recipe/configs/snippets/archipelago-https-app-proxies.conf` — `/app/{id}/` proxy in HTTPS block
|
|
- [ ] Any custom status endpoints (e.g., `/electrs-status`) proxied before the SPA catch-all
|
|
|
|
### Deploy & First Boot
|
|
|
|
- [ ] `scripts/deploy-to-target.sh` — container creation/update logic
|
|
- [ ] `scripts/first-boot-containers.sh` — container created on fresh ISO install
|
|
- [ ] Custom UI containers (e.g., electrs-ui): built and started in both deploy and first-boot
|
|
|
|
### ISO Build
|
|
|
|
- [ ] `image-recipe/build-auto-installer-iso.sh` — `CAPTURE_PATTERNS`: image captured from live server
|
|
- [ ] `image-recipe/build-auto-installer-iso.sh` — `CONTAINER_IMAGES`: fallback image pulled from registry
|
|
- [ ] `image-recipe/build-auto-installer-iso.sh` — docker UI source files bundled for build fallback
|
|
- [ ] `image-recipe/build-auto-installer-iso.sh` — installer copies files to target disk
|
|
|
|
### Runtime Verification
|
|
|
|
- [ ] Test the app UI loads on its configured port
|
|
- [ ] Auto-connect dependencies (Bitcoin RPC, LND, etc.) — apps must work out of the box
|
|
- [ ] Most apps launch in iframe; BTCPay (23000) and Home Assistant (8123) open in new tab (X-Frame-Options)
|
|
|
|
## ISO Build
|
|
|
|
Build on the target server (has all dependencies):
|
|
|
|
```bash
|
|
ssh archipelago@192.168.1.228
|
|
cd ~/archy/image-recipe
|
|
sudo ./build-auto-installer-iso.sh
|
|
# Result: results/archipelago-auto-installer-*.iso
|
|
```
|
|
|
|
After testing on live server, always update ISO build to include changes. Sync system configs:
|
|
- `archipelago.service` → `image-recipe/configs/`
|
|
- `nginx-archipelago.conf` → `image-recipe/configs/`
|
|
|
|
## Key Documentation
|
|
|
|
- `docs/architecture.md` — System architecture
|
|
- `docs/current-state.md` — Current development phase
|
|
- `docs/development-setup.md` — Local dev setup
|
|
- `docs/app-manifest-spec.md` — YAML manifest spec
|
|
- `BUILD-GUIDE.md` — ISO build guide
|
|
- `DEPLOYMENT.md` — Deployment details
|
|
- `CHANGELOG.md` — Version history
|