Add full four-audience wiki: noobs, user, admin, dev
21 pages covering all audiences verified against source code: - Home.md + _Sidebar.md (landing + nav) - Noobs-Start-Here.md (plain-language intro, one-line install) - User-Guide.md, User-Connect-VPN.md, User-Services.md (peer audience) - Admin-Guide.md + 8 subpages: setup, domains/TLS, services, connectivity (named connections, fail-open/closed, cell-relay), peers, backup, logging/audit, monitoring/troubleshooting - Dev-Guide.md + 5 subpages: architecture (connectivity v2 data model, cell-to-cell), building services (manifest v3), API reference, testing, install internals All content verified against api/, install.sh, Makefile, docs/, and pic-services/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
+115
@@ -0,0 +1,115 @@
|
||||
# Backup and Restore
|
||||
|
||||
---
|
||||
|
||||
## What is included in a backup
|
||||
|
||||
A `make backup` archive contains:
|
||||
|
||||
- `config/` — all service configuration, including `cell_config.json`
|
||||
- `data/` — secrets and state (WireGuard keys, internal CA, vault keys, admin credentials, DDNS token, peer data, cell links, connectivity configs), **excluding** logs and internal config-backup snapshots
|
||||
- `docker-compose.yml` and `Makefile`
|
||||
|
||||
The archive is written with permissions `0600`. It contains key material. Store it somewhere safe — anyone with the archive and enough time can recover your credentials.
|
||||
|
||||
The API-driven backup captures everything above, and additionally:
|
||||
|
||||
- `.env`
|
||||
- Caddyfile and Corefile (runtime-generated)
|
||||
- DNS zone files
|
||||
- Vault directory (CA, certificates, trust store)
|
||||
- Per-peer service credentials
|
||||
- Caddy ACME certificates and ACME state
|
||||
- Live service data volumes (email mailboxes, calendar collections, file trees, streamed via `docker exec tar`)
|
||||
|
||||
---
|
||||
|
||||
## Running a backup
|
||||
|
||||
### Via make (quick archive)
|
||||
|
||||
```bash
|
||||
make backup
|
||||
```
|
||||
|
||||
Creates `backups/cell-backup-<timestamp>.tar.gz`. Does **not** include live service data volumes.
|
||||
|
||||
### Via API (full backup with service data)
|
||||
|
||||
```
|
||||
POST /api/config/backup
|
||||
```
|
||||
|
||||
To encrypt the archive with a passphrase:
|
||||
|
||||
```
|
||||
POST /api/config/backup
|
||||
Content-Type: application/json
|
||||
|
||||
{"passphrase": "your-passphrase-here"}
|
||||
```
|
||||
|
||||
The encrypted file is named `<backup_id>.tar.gz.age`. The plaintext staging directory is removed immediately after encryption. Fernet encryption with an scrypt-derived key is used.
|
||||
|
||||
Both the unencrypted and encrypted archive files are written with permissions `0600`.
|
||||
|
||||
---
|
||||
|
||||
## Restoring from a backup
|
||||
|
||||
### From a `make backup` archive
|
||||
|
||||
```bash
|
||||
tar -xzf backups/cell-backup-YYYYMMDD-HHMMSS.tar.gz
|
||||
make restart
|
||||
```
|
||||
|
||||
After restart, the API regenerates the Caddyfile and Corefile from the restored config and re-applies routing rules.
|
||||
|
||||
### From an API backup
|
||||
|
||||
```
|
||||
POST /api/config/restore/<backup_id>
|
||||
```
|
||||
|
||||
For an encrypted archive:
|
||||
|
||||
```
|
||||
POST /api/config/restore/<backup_id>
|
||||
Content-Type: application/json
|
||||
|
||||
{"passphrase": "your-passphrase-here"}
|
||||
```
|
||||
|
||||
The restore process:
|
||||
|
||||
1. Restores the vault first (the vault key must be present before any encrypted secrets can be read)
|
||||
2. Restores identity, `.env`, WireGuard keys, cell links
|
||||
3. Restores Caddy ACME certs, Caddyfile, Corefile, DNS zones
|
||||
4. Restores connectivity configs, auth users, DDNS token
|
||||
5. Restores service account credential files
|
||||
6. Reloads `cell_config.json` into memory
|
||||
7. Restores live service data volumes (if service containers are running)
|
||||
8. Regenerates Caddyfile and Corefile from restored config and re-applies routing rules
|
||||
|
||||
After an API restore, run `make restart` to ensure all containers pick up the restored configuration.
|
||||
|
||||
---
|
||||
|
||||
## Backup schedule recommendation
|
||||
|
||||
PIC does not run automated backups on a schedule. Set up a cron job or systemd timer to call `make backup` (or the API endpoint) regularly.
|
||||
|
||||
Example cron (runs nightly at 2 AM from the PIC directory):
|
||||
|
||||
```
|
||||
0 2 * * * cd /opt/pic && make backup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Important
|
||||
|
||||
A backup contains everything needed to fully reconstruct your cell — including your WireGuard private key, internal CA, and admin credentials. Treat backup files with the same security as you would a password manager export.
|
||||
|
||||
If you use passphrase encryption, store the passphrase separately from the backup file. If you lose the passphrase, the backup is unrecoverable.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Connectivity
|
||||
|
||||
The Connectivity feature lets you route individual peers (or services) through an alternate exit instead of the cell's default internet gateway. This is useful for privacy, geographic routing, or compliance requirements.
|
||||
|
||||
---
|
||||
|
||||
## Concepts
|
||||
|
||||
### Connection instances
|
||||
|
||||
A **connection** is a named, configured instance of an exit type. You create as many as you need and give each a name (e.g. "Home VPN", "US Exit", "Tor").
|
||||
|
||||
Once a connection is created, you assign peers to it. Each peer uses exactly one connection (or the default gateway if none is assigned).
|
||||
|
||||
Connection types:
|
||||
|
||||
| Type | Mechanism | Required service |
|
||||
|---|---|---|
|
||||
| `wireguard_ext` | WireGuard client tunnel to an external server | `wireguard-ext` |
|
||||
| `openvpn` | OpenVPN client tunnel | `openvpn-client` |
|
||||
| `tor` | Transparent proxy through Tor SOCKS | `tor` |
|
||||
| `sshuttle` | SSH tunnel via sshuttle | `sshuttle` |
|
||||
| `proxy` | HTTP/SOCKS5 upstream proxy via redsocks | `proxy` |
|
||||
|
||||
**Tor is limited to one instance per cell.** All other types support multiple named instances.
|
||||
|
||||
### Fail-open vs fail-closed
|
||||
|
||||
When the exit a peer is assigned to goes down, PIC decides whether to block the peer's traffic or let it fall back to the direct internet:
|
||||
|
||||
- **Fail-closed** (default for all types except Tor): if the exit is down, traffic from that peer is blocked by a kill-switch FORWARD rule in `cell-wireguard`. The peer loses internet access until the exit recovers.
|
||||
- **Fail-open** (default for Tor only): if the exit is down, traffic falls back to the default gateway.
|
||||
|
||||
You can override the fail-open behaviour per peer with `PUT /api/connectivity/peers/<peer_name>/failopen`.
|
||||
|
||||
### Cell-relay connections
|
||||
|
||||
A **cell-relay** connection is auto-derived from a cell-to-cell link where the remote cell offers its internet exit. You do not create these manually — they appear automatically in the connection list when a linked cell advertises an exit. Assigning a peer to a cell-relay connection routes their traffic through the remote cell's WAN. Cell-relay connections fail-closed by default.
|
||||
|
||||
---
|
||||
|
||||
## Setting up a connection
|
||||
|
||||
1. Install the corresponding exit service from the Services catalog first (e.g. install `wireguard-ext` before creating a `wireguard_ext` connection).
|
||||
2. Go to **Connectivity** in the sidebar.
|
||||
3. Click **Add Connection**.
|
||||
4. Choose the type, give it a name, and fill in the required config (e.g. upload the WireGuard config file for `wireguard_ext`, or supply the host/user/port for `sshuttle`).
|
||||
5. Click **Save**.
|
||||
|
||||
The connection now appears in the list with a health status indicator.
|
||||
|
||||
---
|
||||
|
||||
## Assigning a peer to a connection
|
||||
|
||||
1. Go to **Connectivity** and select the connection.
|
||||
2. In the **Peers** tab, add the peer by name.
|
||||
3. Click **Apply**.
|
||||
|
||||
Alternatively, from the **Peers** page, open a peer and set the **Exit** field.
|
||||
|
||||
PIC immediately applies the routing change in `cell-wireguard` using `fwmark` and policy routing tables. No restart is needed.
|
||||
|
||||
---
|
||||
|
||||
## Assigning a service to a connection
|
||||
|
||||
Services that declare `has_egress: true` in their manifest (typically the email service) can have their outbound traffic routed through a specific exit. From the Connectivity page, select a connection and go to the **Services** tab.
|
||||
|
||||
---
|
||||
|
||||
## Connection health
|
||||
|
||||
Each connection has a health status: `working`, `down`, or `unknown`. PIC probes health on demand when you load the Connectivity page, and caches the result for 30 seconds. You can also request an immediate probe:
|
||||
|
||||
```
|
||||
GET /api/connectivity/connections/<conn_id>/health
|
||||
```
|
||||
|
||||
Health check mechanism by type:
|
||||
|
||||
| Type | How health is checked |
|
||||
|---|---|
|
||||
| `wireguard_ext` | Last WireGuard handshake age (down if older than 3 minutes) |
|
||||
| `openvpn` | Interface presence check |
|
||||
| `tor` | TCP connect to Tor SOCKS port |
|
||||
| `sshuttle` | TCP connect to the sshuttle transparent proxy port |
|
||||
| `proxy` | TCP connect to the redsocks proxy port |
|
||||
|
||||
---
|
||||
|
||||
## Connectivity and routing internals
|
||||
|
||||
PIC implements exit routing using `fwmark` and `ip rule` / `ip route` inside the `cell-wireguard` container. Each connection instance is allocated:
|
||||
|
||||
- A unique fwmark (starting from `0x1000`, stride `0x10`)
|
||||
- A dedicated routing table (starting from table `1000`)
|
||||
- For `wireguard_ext` and `openvpn`: a named interface (e.g. `wgext_<suffix>`, `ovpn_<suffix>`)
|
||||
- For `tor`, `sshuttle`, `proxy`: a redirect port (starting from `9100`)
|
||||
|
||||
This is the v2 connectivity model. If you have a cell upgraded from an older version, PIC automatically migrates the single legacy exit configuration to a named connection instance on first access.
|
||||
|
||||
---
|
||||
|
||||
## Removing a connection
|
||||
|
||||
A connection cannot be removed while peers are assigned to it. First unassign all peers (set their exit to `default`), then delete the connection from the Connectivity page. Deleting a connection that is still in use returns HTTP 409.
|
||||
|
||||
---
|
||||
|
||||
## Cell-to-cell links
|
||||
|
||||
Cell-to-cell links (site-to-site WireGuard tunnels between two PIC cells) are managed from the **Cell Network** page, not the Connectivity page. When a remote cell offers its internet exit, the resulting `cell_relay` connection appears automatically in the Connectivity page's connection list and can be assigned to peers like any other connection.
|
||||
|
||||
See the [Cell Network section in Dev Architecture](Dev-Architecture) for the technical details.
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
# Domains and TLS
|
||||
|
||||
PIC supports five domain modes. The mode determines how your cell gets an HTTPS certificate and whether it registers a public DNS name.
|
||||
|
||||
---
|
||||
|
||||
## Domain modes
|
||||
|
||||
| Mode | Certificate | Use case |
|
||||
|---|---|---|
|
||||
| `pic_ngo` | Wildcard Let's Encrypt via DNS-01; subdomain `<cell-name>.pic.ngo` | Internet-facing; recommended for most users |
|
||||
| `cloudflare` | Wildcard Let's Encrypt via Cloudflare DNS-01 | Bring-your-own domain with Cloudflare DNS |
|
||||
| `duckdns` | Let's Encrypt via DuckDNS DNS-01 | Bring-your-own DuckDNS subdomain |
|
||||
| `http01` | Let's Encrypt per-subdomain cert via HTTP-01 | Cell reachable on port 80; no wildcard cert |
|
||||
| `lan` | Internal CA only | LAN-only; no internet required |
|
||||
|
||||
### `pic_ngo`
|
||||
|
||||
PIC registers `<cell-name>.pic.ngo` with the `ddns.pic.ngo` service during setup. A background thread re-checks and re-publishes your public IP every 5 minutes. The DDNS service issues a wildcard Let's Encrypt certificate via DNS-01 challenge, so all subdomains (`mail.*`, `calendar.*`, `files.*`, etc.) are covered by one cert.
|
||||
|
||||
**Requirements:**
|
||||
- Accurate host clock — see [Host NTP](#host-ntp) below
|
||||
- Port 80 and 443 reachable from the internet (for the ACME challenge and for clients)
|
||||
|
||||
### `cloudflare`
|
||||
|
||||
You provide a Cloudflare API token with DNS-edit permissions. PIC uses it to complete the DNS-01 challenge and update A records when your IP changes.
|
||||
|
||||
### `duckdns`
|
||||
|
||||
You provide a DuckDNS token and subdomain. PIC completes the DNS-01 challenge through the DuckDNS API.
|
||||
|
||||
### `http01`
|
||||
|
||||
Let's Encrypt issues individual certificates for each subdomain by making an HTTP request to your cell on port 80. There is no wildcard — each new service subdomain requires its own challenge. Port 80 must be publicly reachable.
|
||||
|
||||
### `lan`
|
||||
|
||||
PIC's internal certificate authority (managed by `VaultManager`) issues TLS certificates. No internet access is required. Clients that need to trust your cell's HTTPS need to import the internal CA certificate, available from the Vault page in the admin dashboard.
|
||||
|
||||
---
|
||||
|
||||
## How TLS works in practice
|
||||
|
||||
Caddy handles all TLS termination. When a new service is installed, PIC regenerates the Caddyfile and either reloads Caddy hot (for most changes) or requests a new certificate from Let's Encrypt. Internal services get a cert from the internal CA.
|
||||
|
||||
Certificate status (expiry date, days remaining) is visible from the Settings page under **Vault & Trust**, or via:
|
||||
|
||||
```
|
||||
GET /api/caddy/cert-status
|
||||
```
|
||||
|
||||
You can force a renewal attempt:
|
||||
|
||||
```
|
||||
POST /api/caddy/cert-renew
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Split-horizon DNS
|
||||
|
||||
PIC's CoreDNS (`cell-dns`) serves a split-horizon configuration for your cell domain:
|
||||
|
||||
- **Inside the VPN:** the cell domain resolves to the WireGuard IP of `cell-caddy`. Traffic flows through the tunnel.
|
||||
- **Outside the VPN:** the DDNS provider's A record points to your public IP.
|
||||
|
||||
This means the same URL works on and off the VPN. The DNS answer changes depending on which network the client is on.
|
||||
|
||||
The `.cell` TLD is served authoritatively by CoreDNS and resolves only for VPN-connected clients.
|
||||
|
||||
---
|
||||
|
||||
## DDNS heartbeat
|
||||
|
||||
For `pic_ngo`, `cloudflare`, and `duckdns` modes, a background thread in the API runs every 5 minutes. It checks your current public IP and calls the provider's update API only when the IP has changed.
|
||||
|
||||
DDNS configuration lives in `config/api/cell_config.json` under the `ddns` key. The bearer token (for `pic_ngo`) is stored in `data/api/.ddns_token`.
|
||||
|
||||
Registration uses a time-based OTP (TOTP) to prevent unauthorised subdomain registration on `pic.ngo`. This is why accurate host time is required.
|
||||
|
||||
---
|
||||
|
||||
## Host NTP
|
||||
|
||||
The installer automatically installs and enables `chrony` on the host. This is required for two things:
|
||||
|
||||
1. **ACME certificate issuance** — Let's Encrypt rejects challenges with a skewed clock.
|
||||
2. **DDNS registration** — the `pic.ngo` registration endpoint requires a valid TOTP, which depends on the clock.
|
||||
|
||||
If you installed manually without installing chrony, do it now:
|
||||
|
||||
```bash
|
||||
# Debian / Ubuntu
|
||||
sudo apt-get install -y chrony && sudo systemctl enable --now chrony
|
||||
|
||||
# Fedora / RHEL
|
||||
sudo dnf install -y chrony && sudo systemctl enable --now chronyd
|
||||
|
||||
# Alpine
|
||||
sudo apk add chrony && sudo rc-update add chronyd default && sudo service chronyd start
|
||||
```
|
||||
|
||||
The `cell-ntp` container (chrony) serves NTP to VPN-connected peers separately from the host chrony.
|
||||
|
||||
---
|
||||
|
||||
## Changing the domain mode
|
||||
|
||||
You can change domain modes from the Settings page in the admin dashboard. Changing the mode:
|
||||
|
||||
1. Updates the identity config
|
||||
2. Regenerates the Caddyfile
|
||||
3. Re-registers or deregisters the DDNS subdomain as appropriate
|
||||
4. Rebuilds CoreDNS split-horizon zones
|
||||
|
||||
If you change from `pic_ngo` to `lan`, the DDNS registration is not automatically removed. You will need to do that manually if needed.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Admin Guide
|
||||
|
||||
This is the operator manual for a Personal Internet Cell. It covers everything a cell admin needs to know after the initial install.
|
||||
|
||||
---
|
||||
|
||||
## Sections
|
||||
|
||||
| Page | What it covers |
|
||||
|---|---|
|
||||
| [Setup and First Run](Admin-Setup) | The first-run wizard, cell identity, and what the installer does |
|
||||
| [Domains and TLS](Admin-Domains-TLS) | Domain modes, HTTPS certificates, DDNS, and chrony |
|
||||
| [Services Catalog](Admin-Services) | Installing and removing optional services; what each service provides |
|
||||
| [Connectivity](Admin-Connectivity) | Named VPN/tunnel/proxy connections, per-peer routing, fail-open/fail-closed, cell-to-cell links |
|
||||
| [Peers](Admin-Peers) | Adding peers, WireGuard config export, per-peer access control |
|
||||
| [Backup and Restore](Admin-Backup) | What is backed up, passphrase encryption, how to restore |
|
||||
| [Logging and Audit](Admin-Logging-Audit) | Per-service log levels, activity audit trail |
|
||||
| [Monitoring and Troubleshooting](Admin-Monitoring-Troubleshooting) | Health checks, common errors, and fixes |
|
||||
|
||||
---
|
||||
|
||||
## Quick reference — daily commands
|
||||
|
||||
Run these from the PIC installation directory (`/opt/pic` for installer installs):
|
||||
|
||||
```bash
|
||||
make status # container status + API health check
|
||||
make logs # follow all container logs
|
||||
make logs-api # follow API logs
|
||||
make update # pull latest code, rebuild, restart
|
||||
make backup # archive config/ and data/ to backups/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security reminders
|
||||
|
||||
- The Flask API (port 3000) is bound to `127.0.0.1` only. Caddy proxies all external requests. Do not expose port 3000 externally — access to it is equivalent to root access on the host.
|
||||
- All secrets (WireGuard keys, CA private key, admin credentials, DDNS token) live in `data/` which is git-ignored. Keep your backups safe — they contain this material.
|
||||
- The Docker socket is mounted only into the `cell-api` container. No other container has Docker access.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Logging and Audit
|
||||
|
||||
---
|
||||
|
||||
## Log levels
|
||||
|
||||
PIC has two separate log-level axes:
|
||||
|
||||
1. **Python service log levels** — control verbosity for the Flask API's manager modules (e.g. `network`, `wireguard`, `email`). Changes apply hot to the running process.
|
||||
2. **Container log levels** — control verbosity inside specific containers (Caddy, CoreDNS, WireGuard, mailserver). Some changes apply hot; others require a container restart.
|
||||
|
||||
### Python log levels
|
||||
|
||||
Each manager module has its own logger. You can set them individually or set a root level that applies to everything not otherwise specified.
|
||||
|
||||
Available levels: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`.
|
||||
|
||||
Retrieve current levels:
|
||||
|
||||
```
|
||||
GET /api/logs/verbosity
|
||||
```
|
||||
|
||||
Update levels:
|
||||
|
||||
```
|
||||
PUT /api/logs/verbosity
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"python": {
|
||||
"root": "INFO",
|
||||
"services": {
|
||||
"network": "DEBUG",
|
||||
"wireguard": "DEBUG",
|
||||
"email": "INFO"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Valid service names for the `services` map: `network`, `wireguard`, `email`, `calendar`, `files`, `routing`, `vault`.
|
||||
|
||||
### Container log levels
|
||||
|
||||
Pass a `"containers"` key in the same request:
|
||||
|
||||
```
|
||||
PUT /api/logs/verbosity
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"containers": {
|
||||
"caddy": "DEBUG",
|
||||
"coredns": "INFO"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The response includes an `"applied"` map showing `"hot"` or `"pending_restart"` per container. `caddy` and `coredns` apply hot (Caddy is reloaded; CoreDNS is reloaded via SIGUSR1). `wireguard` and `mailserver` require a container restart for the level change to take effect.
|
||||
|
||||
---
|
||||
|
||||
## Log files
|
||||
|
||||
Logs are written to `data/logs/` inside the API container (`/app/logs/` inside the container). Each service has its own log file with rotation at 5 MB and 5 backups.
|
||||
|
||||
Retrieve the last N lines for a service:
|
||||
|
||||
```
|
||||
GET /api/logs?service=wireguard&lines=100
|
||||
```
|
||||
|
||||
Search logs:
|
||||
|
||||
```
|
||||
GET /api/logs/search?q=<query>&service=<service>
|
||||
```
|
||||
|
||||
From the command line:
|
||||
|
||||
```bash
|
||||
make logs # follow all container logs via docker compose
|
||||
make logs-api # follow the api container log
|
||||
make logs-wireguard # follow the wireguard container log
|
||||
make logs-caddy # follow the caddy container log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Activity audit log
|
||||
|
||||
PIC maintains an append-only, hash-chained audit trail of administrative actions. Every mutating API request (`POST`, `PUT`, `DELETE`, `PATCH`) is recorded with:
|
||||
|
||||
- Who — username, role, and source IP
|
||||
- What — action type and target (e.g. `peer_add`, `peer:laptop`)
|
||||
- When — UTC timestamp
|
||||
- Result — `success` or `failure`
|
||||
- Summary — a human-readable description; key **names** only, never values (secrets are never written)
|
||||
|
||||
The chain uses a per-entry SHA-256 hash linking each entry to the previous one. Tampering with the log file is detectable by verifying the chain.
|
||||
|
||||
The audit log is admin-only. Peer sessions cannot read it.
|
||||
|
||||
### Querying the audit log
|
||||
|
||||
```
|
||||
GET /api/audit
|
||||
GET /api/audit?actor=admin&action=peer_add&limit=50
|
||||
GET /api/audit?since=2026-01-01T00:00:00Z&until=2026-06-01T00:00:00Z
|
||||
```
|
||||
|
||||
Filter parameters: `actor`, `action`, `target_type`, `target_id`, `result`, `since`, `until`. Pagination via `limit` (default 100) and `offset`.
|
||||
|
||||
### Exporting the audit log
|
||||
|
||||
```
|
||||
GET /api/audit/export?format=csv
|
||||
```
|
||||
|
||||
Returns a CSV file attachment.
|
||||
|
||||
### Verifying the chain
|
||||
|
||||
```
|
||||
GET /api/audit/verify
|
||||
```
|
||||
|
||||
Returns whether the hash chain is intact.
|
||||
|
||||
### Storage
|
||||
|
||||
The audit log lives at `data/api/audit/audit.log` (a JSONL file). It rotates at 10 MB with 10 backup files kept. The audit log is included in both `make backup` and API-driven backups.
|
||||
|
||||
### What is not audited
|
||||
|
||||
Read-only requests (`GET`) and diagnostic POST endpoints (e.g. connectivity test) are not recorded. The audit log tracks state changes only.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Monitoring and Troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## Checking overall health
|
||||
|
||||
```bash
|
||||
make status # container status + API health check at /health
|
||||
```
|
||||
|
||||
The `/health` endpoint is always public (no auth required). It returns a JSON object with the running state of each core component. A background thread in the API re-checks health every 30 seconds and keeps a recent history:
|
||||
|
||||
```
|
||||
GET /health
|
||||
GET /api/health/history
|
||||
GET /api/status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common problems
|
||||
|
||||
### API returns 428 and redirects to /setup
|
||||
|
||||
The first-run wizard has not been completed. Open `http://<host-ip>:8081` in a browser and finish the wizard.
|
||||
|
||||
### API returns 401 / "Not authenticated"
|
||||
|
||||
Your session expired. Open `http://<host-ip>:8081/login`.
|
||||
|
||||
### API returns 503 "Authentication not configured"
|
||||
|
||||
The auth file exists but is empty. This should not happen in normal operation, but if it does:
|
||||
|
||||
```bash
|
||||
make reset-admin-password
|
||||
```
|
||||
|
||||
This prints a new random admin password.
|
||||
|
||||
### Forgot the admin password
|
||||
|
||||
```bash
|
||||
make show-admin-password # print the current password
|
||||
make reset-admin-password # generate and set a new random password
|
||||
```
|
||||
|
||||
### Containers not starting
|
||||
|
||||
```bash
|
||||
make logs
|
||||
make logs-api
|
||||
```
|
||||
|
||||
Look for error messages about missing config files, port conflicts, or Docker socket errors.
|
||||
|
||||
### Port 53 already in use
|
||||
|
||||
On Ubuntu and Debian, `systemd-resolved` listens on port 53. Disable it:
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now systemd-resolved
|
||||
sudo rm /etc/resolv.conf
|
||||
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
|
||||
```
|
||||
|
||||
Then run `make start`.
|
||||
|
||||
### WireGuard container fails to start
|
||||
|
||||
The WireGuard container runs unprivileged (`NET_ADMIN` capability only — not full `privileged` mode). It requires the WireGuard kernel module on the host. Load it manually:
|
||||
|
||||
```bash
|
||||
sudo modprobe wireguard
|
||||
```
|
||||
|
||||
On kernels without a built-in WireGuard module, install the appropriate package for your distribution (`wireguard-dkms` or `wireguard-linux-compat`).
|
||||
|
||||
### VPN connects but no DNS
|
||||
|
||||
Split-horizon DNS is not working. Check:
|
||||
|
||||
1. The WireGuard client config includes the cell's VPN IP (`10.0.0.1`) as the DNS server.
|
||||
2. CoreDNS is running: `make logs-dns`.
|
||||
3. The zone file for your cell domain exists: `make shell-dns` then check for zone files.
|
||||
|
||||
### HTTPS not working / certificate error
|
||||
|
||||
1. Check the TLS certificate status:
|
||||
```
|
||||
GET /api/caddy/cert-status
|
||||
```
|
||||
2. If the cert is expired or missing, check the host clock: `date`. If it is wrong, fix chrony (see [Domains and TLS](Admin-Domains-TLS)).
|
||||
3. For `lan` mode, clients need to trust the internal CA. Download it from the Vault page and add it to the client's trust store.
|
||||
4. For `pic_ngo` / `cloudflare` / `duckdns`, port 80 and 443 must be reachable from the internet.
|
||||
|
||||
### Domain resolves to wrong IP
|
||||
|
||||
The DDNS heartbeat runs every 5 minutes. If your IP changed recently, wait a few minutes and check again. You can force a refresh from the Network Services page, or:
|
||||
|
||||
```
|
||||
GET /api/dns/refresh
|
||||
```
|
||||
|
||||
### Service not loading after install
|
||||
|
||||
Check the service's container status:
|
||||
|
||||
```
|
||||
GET /api/services/catalog/<id>/status
|
||||
```
|
||||
|
||||
Check the service logs from the Logs page in the UI (select the service) or via the API:
|
||||
|
||||
```
|
||||
GET /api/logs?service=<service_name>
|
||||
```
|
||||
|
||||
### Cell-to-cell traffic not routing
|
||||
|
||||
Check:
|
||||
|
||||
1. The cell link is active and the WireGuard handshake is recent: check the Cell Network page.
|
||||
2. The iptables FORWARD rules are in place: check from the Routing page or the `FirewallManager` log.
|
||||
3. There is no exit-routing loop (peer A assigned to a cell-relay to cell B, which routes back to A).
|
||||
|
||||
---
|
||||
|
||||
## Opening a shell in a container
|
||||
|
||||
```bash
|
||||
make shell-api # Flask API container
|
||||
make shell-dns # CoreDNS container
|
||||
make shell-wireguard # WireGuard container (useful for wg show, ip rule list)
|
||||
make shell-caddy # Caddy container
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checking WireGuard routing tables
|
||||
|
||||
Inside the WireGuard container:
|
||||
|
||||
```bash
|
||||
make shell-wireguard
|
||||
# then:
|
||||
wg show
|
||||
ip rule list
|
||||
ip route list table <table-number>
|
||||
```
|
||||
|
||||
Connectivity tables start from 1000. Each named connection instance uses a dedicated table and fwmark.
|
||||
|
||||
---
|
||||
|
||||
## Full reinstall
|
||||
|
||||
If the cell is in an unrecoverable state and you have a backup:
|
||||
|
||||
```bash
|
||||
make uninstall # stop and optionally wipe config/ and data/
|
||||
# restore from backup
|
||||
tar -xzf backups/cell-backup-YYYYMMDD-HHMMSS.tar.gz
|
||||
make start
|
||||
```
|
||||
|
||||
To start completely fresh (no restore):
|
||||
|
||||
```bash
|
||||
make reinstall # full wipe of config/ and data/, then setup + start
|
||||
```
|
||||
|
||||
`make reinstall` destroys all data. Run `make backup` first if you have anything worth keeping.
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
# Peers
|
||||
|
||||
A **peer** is a device or person that connects to your cell over WireGuard. Each peer gets a private key pair, a VPN IP address, and optionally an account on installed services.
|
||||
|
||||
---
|
||||
|
||||
## Adding a peer
|
||||
|
||||
1. Go to **Peers** in the sidebar.
|
||||
2. Click **Add Peer**.
|
||||
3. Enter a peer name (letters, digits, underscores, hyphens, dots — max 64 characters).
|
||||
4. Click **Save**. PIC generates a key pair and assigns the next available VPN IP automatically.
|
||||
|
||||
---
|
||||
|
||||
## Exporting a peer configuration
|
||||
|
||||
After adding a peer:
|
||||
|
||||
- Click the **QR code** icon to show the configuration as a QR code. Scan it with the WireGuard mobile app.
|
||||
- Click the **Download** icon to download the `.conf` file. Import it into the WireGuard desktop app.
|
||||
|
||||
The configuration file contains the peer's private key. Treat it like a password. If a peer loses their configuration, you cannot retrieve the private key — delete the peer and create a new one.
|
||||
|
||||
---
|
||||
|
||||
## Peer VPN IPs
|
||||
|
||||
VPN IPs are assigned sequentially from the WireGuard subnet (`10.0.0.0/24` by default). The cell's VPN address is `10.0.0.1`. Peers start from `10.0.0.2`.
|
||||
|
||||
The IP is automatically included in the exported config's `AllowedIPs` field.
|
||||
|
||||
---
|
||||
|
||||
## Per-peer routing
|
||||
|
||||
From the peer's detail page, you can set the peer's **exit connection** (default, or a named connection from the Connectivity page). See [Connectivity](Admin-Connectivity) for details.
|
||||
|
||||
You can also set the peer's **fail-open** override:
|
||||
|
||||
- `null` — use the type's default (fail-closed for VPN/SSH/proxy, fail-open for Tor)
|
||||
- `true` — fall back to direct internet if the exit is down
|
||||
- `false` — block traffic if the exit is down
|
||||
|
||||
---
|
||||
|
||||
## Per-peer service access
|
||||
|
||||
From the service pages (e.g. `/services/email`, `/services/calendar`), you can provision or revoke a peer's account on that service. When a peer is deleted, all their service accounts are removed automatically.
|
||||
|
||||
---
|
||||
|
||||
## Deleting a peer
|
||||
|
||||
1. Go to **Peers**, find the peer, and click **Delete**.
|
||||
2. Confirm. PIC removes the peer from `wg0.conf`, removes their accounts on all installed services, and revokes any cell-to-cell routing entries for that peer.
|
||||
|
||||
The peer's WireGuard config (which they have on their device) stops working immediately after deletion — the server-side peer entry is gone.
|
||||
|
||||
---
|
||||
|
||||
## Listing peers from the command line
|
||||
|
||||
```bash
|
||||
make list-peers # calls the API and prints peer names and VPN IPs
|
||||
make show-routes # runs wg show inside the cell-wireguard container
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Peer roles
|
||||
|
||||
Two roles exist:
|
||||
|
||||
| Role | Access |
|
||||
|---|---|
|
||||
| `admin` | Full access to all API endpoints and the admin dashboard |
|
||||
| `peer` | Access to `/api/peer/*` only — their own dashboard and service credentials |
|
||||
|
||||
Peers created through the Peers page are `peer` role. The `admin` account is created during setup. There can only be one admin; additional admins are not currently supported.
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# Services Catalog
|
||||
|
||||
Optional services are installed from the built-in service store. None of them run by default — only the six core containers start automatically.
|
||||
|
||||
---
|
||||
|
||||
## Core containers (always running)
|
||||
|
||||
| Container | Purpose |
|
||||
|---|---|
|
||||
| `cell-caddy` | Caddy reverse proxy, TLS termination |
|
||||
| `cell-dns` | CoreDNS — `.cell` TLD and split-horizon DNS |
|
||||
| `cell-ntp` | chrony NTP server for VPN peers |
|
||||
| `cell-wireguard` | WireGuard VPN (NET_ADMIN only, not privileged) |
|
||||
| `cell-api` | Flask REST API |
|
||||
| `cell-webui` | React web UI (Nginx) |
|
||||
|
||||
DHCP was removed from PIC. The `cell-dns` container runs CoreDNS only.
|
||||
|
||||
---
|
||||
|
||||
## Available store services
|
||||
|
||||
| Service ID | Name | What it provides |
|
||||
|---|---|---|
|
||||
| `email` | Email Server | Postfix + Dovecot SMTP/IMAP; per-peer mailboxes |
|
||||
| `calendar` | Calendar and Contacts | Radicale CalDAV/CardDAV; per-peer calendars and address books |
|
||||
| `files` | File Storage | WebDAV per-user storage + Filegator browser-based file manager |
|
||||
| `webmail` | Webmail | Roundcube webmail UI; requires the `email` service |
|
||||
| `wireguard-ext` | WireGuard External Exit | Route selected peers through an external WireGuard server |
|
||||
| `openvpn-client` | OpenVPN Exit | Route selected peers through an OpenVPN server |
|
||||
| `tor` | Tor Exit | Route selected peers through Tor (transparent proxy) |
|
||||
| `sshuttle` | SSH Tunnel Exit | Route selected peers through an SSH server via sshuttle |
|
||||
| `proxy` | HTTP/SOCKS5 Proxy Exit | Route selected peers through an upstream proxy via redsocks |
|
||||
|
||||
---
|
||||
|
||||
## Installing a service
|
||||
|
||||
1. Go to **Services** in the left sidebar.
|
||||
2. Find the service card.
|
||||
3. Click **Install**. PIC fetches the manifest from `git.pic.ngo/roof/pic-services`, validates it, starts the container(s), and wires up DNS and Caddy routes automatically.
|
||||
4. The service card shows a progress state while installation runs. Reload the page to check status.
|
||||
5. Once installed, the service appears in the sidebar navigation.
|
||||
|
||||
---
|
||||
|
||||
## Using installed services
|
||||
|
||||
Each installed service gets a subdomain. For example, with cell name `myhome` and domain mode `pic_ngo`:
|
||||
|
||||
| Service | URL |
|
||||
|---|---|
|
||||
| Email (webmail, if installed) | `https://mail.myhome.pic.ngo` |
|
||||
| Calendar | `https://calendar.myhome.pic.ngo` |
|
||||
| Files | `https://files.myhome.pic.ngo` |
|
||||
|
||||
IMAP/SMTP ports for email:
|
||||
|
||||
| Protocol | Port | Security |
|
||||
|---|---|---|
|
||||
| IMAP | 993 | SSL/TLS |
|
||||
| SMTP (submission) | 587 | STARTTLS |
|
||||
|
||||
These ports are only opened on the host when the email service is installed.
|
||||
|
||||
---
|
||||
|
||||
## Managing peer accounts on services
|
||||
|
||||
Each installed service can provision per-peer accounts. From the service's admin page (e.g. `/services/email`):
|
||||
|
||||
1. Go to the **Accounts** tab.
|
||||
2. Click **Add Account** and enter the peer's username. PIC generates a password automatically, or you can supply one.
|
||||
3. The peer can see their connection credentials from the peer dashboard.
|
||||
|
||||
To remove a peer's account, click **Remove** next to the account entry. When a peer is deleted from the Peers page, their accounts on all services are also removed.
|
||||
|
||||
---
|
||||
|
||||
## Uninstalling a service
|
||||
|
||||
1. Go to **Services** in the sidebar.
|
||||
2. Click **Uninstall** on the service card.
|
||||
|
||||
The container is stopped and removed. Service data in `data/services/<id>/` is kept on disk. You must delete it manually if you want to free the space. Use the API-driven backup before uninstalling if you want to preserve user data — see [Backup and Restore](Admin-Backup).
|
||||
|
||||
---
|
||||
|
||||
## Service status
|
||||
|
||||
The Services page shows each installed service with a running/stopped indicator. You can also query the API:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:3000/api/services/active
|
||||
```
|
||||
|
||||
To restart a service from the command line, use the container manager or:
|
||||
|
||||
```bash
|
||||
make shell-api
|
||||
# then inside the container:
|
||||
# the service manager's restart endpoint is POST /api/services/catalog/<id>/restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connectivity exit services
|
||||
|
||||
The five connectivity exit services (`wireguard-ext`, `openvpn-client`, `tor`, `sshuttle`, `proxy`) integrate with the Connectivity feature. After installing one, you create a named connection instance from the **Connectivity** page and then assign peers to it. See [Connectivity](Admin-Connectivity) for details.
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
# Setup and First Run
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A — one-line installer (recommended)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.pic.ngo | sudo bash
|
||||
```
|
||||
|
||||
Before running any script as root, review it:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.pic.ngo | less
|
||||
```
|
||||
|
||||
The installer accepts two flags:
|
||||
|
||||
- `--debug` — print verbose output to the terminal instead of only to the log file
|
||||
- `--force` — bypass the idempotency check (re-run on an already-installed host)
|
||||
|
||||
The install log is always written to `/var/log/pic-install.log`. If anything fails without `--debug`, check that file.
|
||||
|
||||
The installer runs 7 steps:
|
||||
|
||||
1. Detects your OS and package manager (apt / dnf / apk)
|
||||
2. Installs Docker, git, make, and host chrony (NTP)
|
||||
3. Creates a `pic` system user and adds it to the `docker` group
|
||||
4. Clones the repository to `/opt/pic`
|
||||
5. Runs `make install` — generates keys, config, and a systemd unit; prints the admin password once
|
||||
6. Runs `make start-core` to bring up the six core containers
|
||||
7. Enables the `pic` systemd unit and waits for the API health check
|
||||
|
||||
When the installer finishes, it prints a URL:
|
||||
|
||||
```
|
||||
Open your browser: http://<host-ip>:8081/setup
|
||||
```
|
||||
|
||||
### Option B — manual install
|
||||
|
||||
Use this if you want to control the install path or if Docker is already installed:
|
||||
|
||||
```bash
|
||||
git clone https://git.pic.ngo/roof/pic.git pic
|
||||
cd pic
|
||||
sudo make install
|
||||
make start-core
|
||||
```
|
||||
|
||||
Install host chrony before running `make install` if you plan to use `pic.ngo` domain mode:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y chrony && sudo systemctl enable --now chrony
|
||||
```
|
||||
|
||||
Then open `http://<host-ip>:8081/setup`.
|
||||
|
||||
---
|
||||
|
||||
## The first-run wizard
|
||||
|
||||
The wizard appears automatically on first start. All API requests redirect to `/setup` (HTTP 428) until the wizard is complete.
|
||||
|
||||
The wizard collects:
|
||||
|
||||
- **Cell name** — used for hostnames and the DDNS subdomain. Must start with a lowercase letter, be 2–31 characters, and contain only lowercase letters, digits, and hyphens. Example: `myhome`.
|
||||
- **Domain mode** — see [Domains and TLS](Admin-Domains-TLS) for details.
|
||||
- **Timezone** — your local timezone, used for log timestamps.
|
||||
- **Services to install** — optional services (email, calendar, files) to start installing in the background after setup completes. You can install them later from the Services page instead.
|
||||
- **Admin password** — minimum 12 characters; must contain at least one uppercase letter, one lowercase letter, and one digit.
|
||||
|
||||
Click **Complete Setup**. The wizard:
|
||||
|
||||
1. Creates the admin account in `data/auth_users.json`
|
||||
2. Writes cell identity to `config/api/cell_config.json`
|
||||
3. Generates the initial Caddyfile and Corefile
|
||||
4. If domain mode is `pic_ngo`, registers `<cell-name>.pic.ngo` with the DDNS service
|
||||
5. Starts any selected services in background threads
|
||||
|
||||
You are redirected to `/login`. Log in with username `admin` and the password you set.
|
||||
|
||||
---
|
||||
|
||||
## Cell identity
|
||||
|
||||
The three core identity values are:
|
||||
|
||||
| Value | Where set | Example |
|
||||
|---|---|---|
|
||||
| Cell name | Wizard / Settings | `myhome` |
|
||||
| Domain mode | Wizard / Settings | `pic_ngo` |
|
||||
| Timezone | Wizard / Settings | `Europe/Berlin` |
|
||||
|
||||
These are stored in `config/api/cell_config.json` under the `_identity` key. Do not edit this file directly — use the Settings page in the UI or the API.
|
||||
|
||||
The cell's WireGuard VPN uses the subnet `10.0.0.0/24` by default (the server address is `10.0.0.1/24`). This is configured in `api/wireguard_manager.py` and is not currently exposed in the wizard UI.
|
||||
|
||||
---
|
||||
|
||||
## After the wizard
|
||||
|
||||
- Go to **Peers** to add devices to the VPN — see [Peers](Admin-Peers).
|
||||
- Go to **Services** to install or manage optional services — see [Services Catalog](Admin-Services).
|
||||
- Go to **Settings** to change domain mode, DDNS config, or admin password.
|
||||
|
||||
---
|
||||
|
||||
## Resetting the admin password
|
||||
|
||||
If you forget the admin password:
|
||||
|
||||
```bash
|
||||
make show-admin-password # print the current password
|
||||
make reset-admin-password # generate and set a new random password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Updating PIC
|
||||
|
||||
```bash
|
||||
make update # git pull + rebuild all images + restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
make uninstall
|
||||
```
|
||||
|
||||
This stops all containers and removes the systemd unit. It then asks whether to also delete `config/` and `data/`. Answering yes performs a full wipe — all configuration, keys, peer data, and service data are deleted and cannot be recovered without a backup.
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
# API Reference
|
||||
|
||||
**Base URL:** `http://localhost:3000` (bound to 127.0.0.1; always access through Caddy in production)
|
||||
|
||||
All `/api/*` endpoints require an authenticated session after setup is complete. The only always-public endpoint is `GET /health`.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Session-based. Two roles:
|
||||
|
||||
| Role | Access |
|
||||
|---|---|
|
||||
| `admin` | All `/api/*` except `/api/peer/*` |
|
||||
| `peer` | `/api/peer/*` only |
|
||||
|
||||
### Login flow
|
||||
|
||||
```
|
||||
POST /api/auth/login
|
||||
{"username": "admin", "password": "..."}
|
||||
```
|
||||
|
||||
Returns a session cookie. All subsequent requests include the cookie.
|
||||
|
||||
### CSRF protection
|
||||
|
||||
All `POST`, `PUT`, `DELETE`, `PATCH` on `/api/*` (except `/api/auth/*` and `/api/setup/*`) require the `X-CSRF-Token` header.
|
||||
|
||||
Fetch the token:
|
||||
|
||||
```
|
||||
GET /api/auth/csrf-token
|
||||
```
|
||||
|
||||
Include it in every mutating request:
|
||||
|
||||
```
|
||||
X-CSRF-Token: <token>
|
||||
```
|
||||
|
||||
### Auth endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | `/api/auth/login` | Create session |
|
||||
| POST | `/api/auth/logout` | Destroy session |
|
||||
| GET | `/api/auth/me` | Current user info |
|
||||
| GET | `/api/auth/csrf-token` | Get CSRF token |
|
||||
| POST | `/api/auth/change-password` | Change own password |
|
||||
| POST | `/api/auth/admin/reset-password` | Admin: reset another user's password |
|
||||
| GET | `/api/auth/users` | Admin: list users |
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/setup/status` | Setup complete flag + current step |
|
||||
| GET | `/api/setup/step` | Current wizard step data |
|
||||
| POST | `/api/setup/step` | Submit current step |
|
||||
| POST | `/api/setup/complete` | Finalize setup |
|
||||
|
||||
---
|
||||
|
||||
## Core
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/health` | Always-public health check |
|
||||
| GET | `/api/status` | All-service status summary |
|
||||
| GET | `/api/config` | Full cell config |
|
||||
| PUT | `/api/config` | Update cell config |
|
||||
| GET | `/api/health/history` | Recent health check history |
|
||||
|
||||
---
|
||||
|
||||
## WireGuard and peers
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/wireguard/status` | WireGuard running state and stats |
|
||||
| GET | `/api/peers` | List all peers |
|
||||
| POST | `/api/peers` | Add a peer |
|
||||
| GET | `/api/peers/<name>` | Get peer details |
|
||||
| PUT | `/api/peers/<name>` | Update peer |
|
||||
| DELETE | `/api/peers/<name>` | Remove peer and their service accounts |
|
||||
| GET | `/api/peers/<name>/qr` | Peer config as QR code |
|
||||
| GET | `/api/peers/<name>/service-credentials` | Filled connection info for all services |
|
||||
|
||||
---
|
||||
|
||||
## Network services
|
||||
|
||||
DNS overview, NTP status, network connectivity tests, split-horizon zone reload, DDNS force-refresh.
|
||||
|
||||
Relevant paths: `/api/dns/*`, `/api/ntp/*`, `/api/network/*`.
|
||||
|
||||
---
|
||||
|
||||
## Services and store
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/services/active` | Installed services with name, subdomain, capabilities |
|
||||
| GET | `/api/store` | Full catalog from `pic-services` index |
|
||||
| POST | `/api/store/install` | Install a service by ID |
|
||||
| POST | `/api/store/remove` | Remove an installed service |
|
||||
| GET | `/api/services/catalog/<id>/status` | Container status for a service |
|
||||
| POST | `/api/services/catalog/<id>/restart` | Restart service containers |
|
||||
| GET | `/api/services/catalog/<id>/accounts` | List provisioned accounts |
|
||||
| POST | `/api/services/catalog/<id>/accounts` | Provision peer account |
|
||||
| DELETE | `/api/services/catalog/<id>/accounts/<username>` | Deprovision peer account |
|
||||
| GET | `/api/services/catalog/<id>/accounts/<username>/credentials` | Get stored credentials |
|
||||
|
||||
---
|
||||
|
||||
## Connectivity (v2)
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/connectivity/connections` | List all connection instances (with status) |
|
||||
| POST | `/api/connectivity/connections` | Create a named connection instance |
|
||||
| PUT | `/api/connectivity/connections/<id>` | Update connection name/config/secrets |
|
||||
| DELETE | `/api/connectivity/connections/<id>` | Delete connection (409 if in use) |
|
||||
| GET | `/api/connectivity/connections/<id>/health` | On-demand health probe |
|
||||
| PUT | `/api/connectivity/peers/<peer>/exit` | Assign peer to a connection |
|
||||
| GET | `/api/connectivity/peers` | List peer exit assignments |
|
||||
| PUT | `/api/connectivity/peers/<peer>/failopen` | Set/clear per-peer fail-open override |
|
||||
| GET | `/api/connectivity/status` | Connectivity overall status |
|
||||
| GET | `/api/connectivity/exits` | Legacy: configured exits summary |
|
||||
|
||||
`POST /api/connectivity/connections` body:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "wireguard_ext",
|
||||
"name": "My VPN",
|
||||
"config": {},
|
||||
"secrets": {"conf": "<wireguard-conf-content>"}
|
||||
}
|
||||
```
|
||||
|
||||
Secrets are stored in the vault. They are never echoed in responses.
|
||||
|
||||
---
|
||||
|
||||
## Audit log
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/audit` | Query audit entries (admin only) |
|
||||
| GET | `/api/audit/export?format=csv` | Export as CSV |
|
||||
| GET | `/api/audit/verify` | Verify hash chain integrity |
|
||||
|
||||
Query parameters for `GET /api/audit`: `actor`, `action`, `target_type`, `target_id`, `result`, `since`, `until`, `limit`, `offset`.
|
||||
|
||||
---
|
||||
|
||||
## Logs
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/logs` | Recent log lines (query: `service`, `lines`) |
|
||||
| GET | `/api/logs/verbosity` | Current Python and container log levels |
|
||||
| PUT | `/api/logs/verbosity` | Update log levels |
|
||||
| GET | `/api/logs/search` | Search logs (query: `q`, `service`) |
|
||||
|
||||
---
|
||||
|
||||
## Vault
|
||||
|
||||
Certificate issue/revoke, CA certificate download, trust key management, Age public key: `/api/vault/*`.
|
||||
|
||||
---
|
||||
|
||||
## Containers
|
||||
|
||||
List, start, stop, inspect containers; manage images and volumes: `/api/containers/*`.
|
||||
|
||||
---
|
||||
|
||||
## Cell network
|
||||
|
||||
List connected cells, add/remove cell links, peer-sync: `/api/cells/*`.
|
||||
|
||||
Cell-to-cell peer-sync endpoints (`/api/cells/peer-sync/*`) authenticate via source IP and WireGuard public key — not session cookies.
|
||||
|
||||
---
|
||||
|
||||
## Email, calendar, files
|
||||
|
||||
These endpoints exist only when the respective service is installed. Non-status routes return HTTP 404 when the service is not installed.
|
||||
|
||||
- `/api/email/*` — mailbox and account management
|
||||
- `/api/calendar/*` — user/calendar/contacts management
|
||||
- `/api/files/*` — WebDAV user and file management
|
||||
|
||||
---
|
||||
|
||||
## Routing and firewall
|
||||
|
||||
NAT rules, per-peer routing policy, exit configuration: `/api/routing/*`.
|
||||
|
||||
---
|
||||
|
||||
## TLS
|
||||
|
||||
```
|
||||
GET /api/caddy/cert-status TLS certificate expiry and domain
|
||||
POST /api/caddy/cert-renew Force certificate renewal
|
||||
```
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# Architecture
|
||||
|
||||
---
|
||||
|
||||
## Container stack
|
||||
|
||||
Six core containers run on a Docker bridge network called `cell-network` (default subnet `172.20.0.0/16`). Static IPs per container are set in `docker-compose.yml` and can be overridden via `.env`.
|
||||
|
||||
```
|
||||
Browser / WireGuard peer
|
||||
└── Caddy (:80/:443) TLS termination, reverse proxy
|
||||
└── React SPA (:8081→8080) Vite + Tailwind (Nginx in container)
|
||||
└── Flask API (:3000) REST API, bound to 127.0.0.1 only
|
||||
├── NetworkManager CoreDNS, chrony
|
||||
├── WireGuardManager WireGuard peer lifecycle
|
||||
├── PeerRegistry peer registration and trust
|
||||
├── EmailManager Postfix + Dovecot
|
||||
├── CalendarManager Radicale CalDAV/CardDAV
|
||||
├── FileManager WebDAV + Filegator
|
||||
├── RoutingManager iptables NAT and routing
|
||||
├── FirewallManager iptables INPUT/FORWARD rules
|
||||
├── VaultManager internal CA, cert lifecycle
|
||||
├── ContainerManager Docker SDK
|
||||
├── CellLinkManager cell-to-cell WireGuard links
|
||||
├── ConnectivityManager exit routing
|
||||
├── DDNSManager DDNS heartbeat
|
||||
├── ServiceStoreManager optional service install/remove
|
||||
├── CaddyManager Caddyfile generation and reload
|
||||
├── AuthManager session auth, RBAC
|
||||
├── AuditManager append-only activity log
|
||||
├── AccountManager per-service account provisioning
|
||||
└── SetupManager first-run wizard state
|
||||
```
|
||||
|
||||
Key container properties:
|
||||
|
||||
- `cell-wireguard` runs unprivileged — `NET_ADMIN` capability only. It requires the WireGuard kernel module on the host. No `--privileged` flag.
|
||||
- `cell-api` and `cell-webui` use slim images.
|
||||
- The Docker socket is mounted only into `cell-api`. Other containers have no Docker access.
|
||||
- The Flask API binds to `127.0.0.1:3000` only. All external access goes through Caddy.
|
||||
- DHCP was removed. `cell-dns` runs CoreDNS only.
|
||||
|
||||
Installed optional service containers join `cell-network` with their own compose projects, managed by `ServiceComposer`. Each service is a separate compose project at `data/services/<id>/docker-compose.yml`.
|
||||
|
||||
---
|
||||
|
||||
## Manager pattern
|
||||
|
||||
All managers inherit `BaseServiceManager` (`api/base_service_manager.py`), which requires implementing:
|
||||
|
||||
- `get_status()` — current running state
|
||||
- `get_config()` / `update_config()` — config read/write
|
||||
- `validate_config()` — validation before write
|
||||
- `test_connectivity()` — reachability check
|
||||
- `get_logs()` — recent log lines
|
||||
- `restart_service()` — container restart via Docker SDK
|
||||
|
||||
Managers are instantiated as singletons in `api/managers.py` and injected into `app.py` as module-level names. Route handlers import them from `app` inside the route function (not at module load time) to avoid circular imports.
|
||||
|
||||
All managers use `self.logger` (from `BaseServiceManager`) and `self.config_manager` for config access. Direct file I/O on `cell_config.json` is a bug.
|
||||
|
||||
Shared state in managers uses `threading.RLock`. Flask is multi-threaded and managers run concurrently.
|
||||
|
||||
---
|
||||
|
||||
## Service bus
|
||||
|
||||
`ServiceBus` (`api/service_bus.py`) is a pub/sub system between managers. Events include `CONFIG_CHANGED`, `SERVICE_STARTED`, `SERVICE_STOPPED`. Managers subscribe to events from their dependencies (e.g., WireGuardManager subscribes to network changes).
|
||||
|
||||
---
|
||||
|
||||
## Config and secrets
|
||||
|
||||
- Runtime config: `config/api/cell_config.json` — managed by `ConfigManager`, never edited directly
|
||||
- Secrets: `data/` — git-ignored; contains `auth_users.json`, WireGuard keys, DDNS token, CA key, vault secrets
|
||||
- `_identity.domain` in cell config is a plain string (the domain mode, e.g. `"pic_ngo"`), not a dict
|
||||
|
||||
`ConfigManager` validates on write and keeps automatic rolling backups in `data/api/config_backups/`.
|
||||
|
||||
---
|
||||
|
||||
## Before-request hooks
|
||||
|
||||
Three Flask before-request hooks run on every request, in order:
|
||||
|
||||
1. `enforce_setup` — returns 428 for all `/api/*` except `/api/setup/*` and `/health` until setup is complete. Skipped when `app.config['TESTING']` is True.
|
||||
2. `enforce_auth` — returns 401 if no session; 503 if the users file is empty (misconfiguration). Skipped when testing.
|
||||
3. `check_csrf` — requires `X-CSRF-Token` header on `POST`, `PUT`, `DELETE`, `PATCH` on `/api/*` except `/api/auth/*` and `/api/setup/*`.
|
||||
|
||||
These are the security boundary. Modifying them requires careful review.
|
||||
|
||||
---
|
||||
|
||||
## Connectivity v2 data model
|
||||
|
||||
The Connectivity feature (v2) uses named connection instances instead of one-global-exit-per-type.
|
||||
|
||||
Each connection is a record in `cell_config.json` under `connectivity.connections`. A record contains:
|
||||
|
||||
- `id` — UUID assigned at creation
|
||||
- `type` — one of `wireguard_ext`, `openvpn`, `tor`, `sshuttle`, `proxy`, `cell_relay`
|
||||
- `name` — human label
|
||||
- `mark` — fwmark hex value, allocated from the pool `0x1000`–`0x1FFF` (stride `0x10`)
|
||||
- `table` — routing table number, starting from `1000`
|
||||
- For iface types (`wireguard_ext`, `openvpn`): `iface` — interface name (`wgext_<suffix>` or `ovpn_<suffix>`)
|
||||
- For redirect types (`tor`, `sshuttle`, `proxy`): `redirect_port` — allocated from `9100`–`9199`
|
||||
- `status` — last health probe result (health, timestamp, detail); never contains secrets
|
||||
- Secrets are stored in the vault under `conn_<id>_<field>` and only the key references are kept in the record
|
||||
|
||||
`cell_relay` connections are auto-derived from cell links that offer an exit. They have mark and table allocated but no iface or redirect_port. They are reconciled automatically on `list_connections()`.
|
||||
|
||||
**Migration v1 to v2:** on first `get_connectivity()` call after upgrade, `ConfigManager` calls `ConnectivityManager._migrate_connectivity_v1_to_v2()` if the stored version is less than 2. This creates one named connection per previously-configured exit type, repoints vault secret references to the new `conn_<id>_<field>` naming, and deletes the old references.
|
||||
|
||||
Peer assignments store the connection `id` as `exit_connection_id` on the peer record. The legacy `route_via` field is kept in sync for backward compatibility.
|
||||
|
||||
---
|
||||
|
||||
## Cell-to-cell networking
|
||||
|
||||
`CellLinkManager` manages WireGuard site-to-site tunnels. Each link is a WireGuard peer on `wg0` with:
|
||||
|
||||
- A `/32` VPN address for the remote cell's API endpoint
|
||||
- `AllowedIPs` covering the remote cell's full VPN subnet
|
||||
|
||||
The peer-sync protocol (`/api/cells/peer-sync/`) allows two cells to exchange public keys and allowed networks without a session. Authentication is by source IP and WireGuard public key — not session cookies.
|
||||
|
||||
When a remote cell advertises that it offers an internet exit, `reconcile_cell_relays()` creates or updates a `cell_relay` connection in the local connectivity config. This is called automatically when `list_connections()` is invoked.
|
||||
|
||||
Access control for cell-to-cell service access (calendar, files, mail, WebDAV) is enforced at the iptables level by `FirewallManager`.
|
||||
|
||||
---
|
||||
|
||||
## Frontend
|
||||
|
||||
The React SPA (`webui/`) is built with Vite and styled with Tailwind CSS utilities. There are no custom CSS files. All API calls go through `webui/src/services/api.js` (Axios). Page components live in `webui/src/pages/`; reusable components in `webui/src/components/`.
|
||||
|
||||
In development, the Vite dev server (`npm run dev`) proxies `/api` requests to `:3000`. In production, Caddy routes them.
|
||||
|
||||
The nav is dynamic: installed services are fetched via `GET /api/services/active` on load. After install or uninstall, the `pic-services-changed` custom DOM event is dispatched to trigger a re-fetch without a full page reload.
|
||||
@@ -0,0 +1,178 @@
|
||||
# Building a Service
|
||||
|
||||
This page is a summary. The full reference is at `docs/service-developer-guide.md` in the PIC repository.
|
||||
|
||||
---
|
||||
|
||||
## What a store service is
|
||||
|
||||
A store service is a Docker container (or set of containers) described by a single JSON manifest file. PIC fetches the manifest when the admin clicks Install, validates it, renders a Docker Compose file, starts the container(s), and wires up DNS and Caddy routes automatically.
|
||||
|
||||
The `email`, `calendar`, and `files` services in `pic-services/services/` are the reference implementations for the full feature set.
|
||||
|
||||
---
|
||||
|
||||
## Manifest schema (v3)
|
||||
|
||||
Every manifest must have `"schema_version": 3` and `"kind": "store"`. Required identity fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 3,
|
||||
"id": "notes",
|
||||
"name": "Notes",
|
||||
"description": "Self-hosted Markdown notes",
|
||||
"version": "1.0.0",
|
||||
"author": "your-name",
|
||||
"kind": "store",
|
||||
"image": "git.pic.ngo/roof/pic-notes:latest",
|
||||
"container_name": "cell-notes"
|
||||
}
|
||||
```
|
||||
|
||||
The `image` field must match `git.pic.ngo/roof/*`. External registries are rejected. The `container_name` must match `^cell-[a-z0-9][a-z0-9-]{0,30}$`.
|
||||
|
||||
### Capabilities
|
||||
|
||||
Six boolean flags tell PIC which integrations to activate:
|
||||
|
||||
| Flag | What it enables |
|
||||
|---|---|
|
||||
| `has_subdomain` | A Caddy reverse-proxy route and a DNS record |
|
||||
| `has_accounts` | Per-peer account provisioning |
|
||||
| `has_admin_config` | A settings form rendered from `config_schema` |
|
||||
| `has_storage` | Backup integration |
|
||||
| `has_egress` | Per-service outbound interface selection |
|
||||
| `has_api_hooks` | Reserved; set `false` |
|
||||
|
||||
### Subdomain and routing
|
||||
|
||||
When `has_subdomain` is true, provide:
|
||||
|
||||
- `subdomain` — the primary subdomain (e.g. `"notes"`). Results in `notes.<cell-domain>`.
|
||||
- `backend` — the `container:port` Caddy proxies to (e.g. `"cell-notes:8080"`).
|
||||
- `extra_subdomains` / `extra_backends` — additional subdomains pointing to different backends (the files service uses this for WebDAV vs Filegator).
|
||||
|
||||
Reserved subdomains that cannot be used: `api`, `webui`, `admin`, `www`, `ns1`, `ns2`, `git`, `registry`, `install`.
|
||||
|
||||
### Volumes and environment
|
||||
|
||||
```json
|
||||
"volumes": [
|
||||
{"name": "notes-data", "mount": "/data/notes"}
|
||||
],
|
||||
"env": [
|
||||
{"key": "NOTES_SECRET", "value": "${PIC_SECRET_NOTES_SECRET}"}
|
||||
]
|
||||
```
|
||||
|
||||
Volume mounts to system paths (`/`, `/etc`, `/var`, `/proc`, `/sys`, `/dev`, `/app`, `/run`, `/boot`) and to paths that overlap the PIC project root are rejected at install time.
|
||||
|
||||
### iptables rules
|
||||
|
||||
To let VPN peers reach your container's port:
|
||||
|
||||
```json
|
||||
"iptables_rules": [
|
||||
{
|
||||
"type": "ACCEPT",
|
||||
"dest_ip": "${SERVICE_IP}",
|
||||
"dest_port": 8080,
|
||||
"proto": "tcp"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Only `ACCEPT` type rules are permitted. `${SERVICE_IP}` is replaced with the allocated container IP at install time.
|
||||
|
||||
### Backup
|
||||
|
||||
When `has_storage` is true:
|
||||
|
||||
```json
|
||||
"backup": {
|
||||
"volumes": [
|
||||
{"container": "cell-notes", "path": "/data/notes", "name": "notes_data"}
|
||||
],
|
||||
"config_paths": ["config/notes"]
|
||||
}
|
||||
```
|
||||
|
||||
Each `volumes` entry is archived via `docker exec <container> tar` and stored as `<name>.tar.gz` in the backup. `config_paths` are host paths copied directly.
|
||||
|
||||
### Account provisioning
|
||||
|
||||
When `has_accounts` is true:
|
||||
|
||||
```json
|
||||
"accounts": {
|
||||
"manager": "http",
|
||||
"credentials": ["password"]
|
||||
},
|
||||
"peer_config_template": {
|
||||
"url": "https://notes.{domain}/",
|
||||
"username": "{peer.username}",
|
||||
"password": "{peer.service_credentials.notes.password}"
|
||||
}
|
||||
```
|
||||
|
||||
For `"manager": "http"`, your container must implement the account management HTTP API at `/service-api/accounts`:
|
||||
|
||||
- `POST /service-api/accounts` — create account (`{"username": "...", "password": "..."}`)
|
||||
- `DELETE /service-api/accounts/{username}` — delete account
|
||||
- `GET /service-api/accounts` — list usernames
|
||||
|
||||
**Note:** HTTP dispatch for the `"http"` manager is not yet wired up in `AccountManager`. The reference services use named internal managers (`email_manager`, `calendar_manager`, `file_manager`). Implement the endpoint now so your service is ready when HTTP dispatch ships.
|
||||
|
||||
### Egress
|
||||
|
||||
When `has_egress` is true:
|
||||
|
||||
```json
|
||||
"egress": {
|
||||
"default": "default",
|
||||
"allowed": ["default", "wireguard_ext", "openvpn", "tor", "sshuttle", "proxy"]
|
||||
}
|
||||
```
|
||||
|
||||
Always include `"default"` in `allowed`.
|
||||
|
||||
---
|
||||
|
||||
## Compose template
|
||||
|
||||
Include a `compose-template.yml` alongside your `manifest.json`. `ServiceComposer` substitutes these variables before writing the per-service `docker-compose.yml`:
|
||||
|
||||
| Variable | Value |
|
||||
|---|---|
|
||||
| `${PIC_DOMAIN}` | Cell's effective domain |
|
||||
| `${PIC_CELL_NAME}` | Short cell name |
|
||||
| `${PIC_DATA_DIR}` | Absolute path to PIC data directory |
|
||||
| `${PIC_SERVICE_ID}` | Service `id` from the manifest |
|
||||
| `${PIC_CFG_<KEY>}` | Admin-configured value for `config_schema` field `<key>` (uppercase) |
|
||||
| `${PIC_SECRET_<NAME>}` | Auto-generated random secret (`secrets.token_urlsafe(24)`); generated once, reused on reconfigure |
|
||||
| `${SERVICE_IP}` | Container IP allocated from `172.20.0.20`–`172.20.0.254` |
|
||||
|
||||
Use named Docker volumes for service data rather than bind mounts to avoid host-path resolution issues.
|
||||
|
||||
---
|
||||
|
||||
## Submitting to the store
|
||||
|
||||
1. Fork `https://git.pic.ngo/roof/pic-services`
|
||||
2. Create `services/<your-id>/manifest.json`
|
||||
3. Add `compose-template.yml`
|
||||
4. Add an entry to `index.json`
|
||||
5. Open a pull request against `main`
|
||||
|
||||
The review checks:
|
||||
|
||||
- Image is at `git.pic.ngo/roof/*`
|
||||
- No volume mounts to system or PIC-root paths
|
||||
- `iptables_rules` are `ACCEPT` only
|
||||
- `subdomain` does not collide with reserved names or existing services
|
||||
- `install.sh` (if included) does not install system packages or run `curl | bash`
|
||||
|
||||
Increment `version` in `manifest.json` with every submission. PIC does not auto-update installed services.
|
||||
|
||||
See `docs/service-developer-guide.md` in the PIC repo for the complete field reference.
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
# Developer Guide
|
||||
|
||||
This guide is for contributors to PIC and for developers building services for the PIC service store.
|
||||
|
||||
---
|
||||
|
||||
## Sections
|
||||
|
||||
| Page | What it covers |
|
||||
|---|---|
|
||||
| [Architecture](Dev-Architecture) | Container stack, manager pattern, service bus, data model |
|
||||
| [Building a Service](Dev-Building-Services) | Manifest schema v3, compose templates, account provisioning, backup, egress |
|
||||
| [API Reference](Dev-API) | Auth model, CSRF, key endpoints, connectivity v2 API |
|
||||
| [Testing](Dev-Testing) | Unit tests (pytest), webui (vitest), integration, e2e, CI |
|
||||
| [Install Internals](Dev-Install-Internals) | install.sh steps, make targets, systemd unit, PIC_DEBUG |
|
||||
|
||||
---
|
||||
|
||||
## Quick start for local development
|
||||
|
||||
```bash
|
||||
# Start the full stack (builds api and webui images)
|
||||
git clone https://git.pic.ngo/roof/pic.git pic
|
||||
cd pic
|
||||
make start
|
||||
# open http://<host-ip>:8081 — wizard appears automatically
|
||||
```
|
||||
|
||||
Run the Flask API without Docker (useful for rapid iteration):
|
||||
|
||||
```bash
|
||||
pip install -r api/requirements.txt
|
||||
python api/app.py # listens on :3000
|
||||
```
|
||||
|
||||
Run the React UI dev server (proxies `/api` to `:3000`):
|
||||
|
||||
```bash
|
||||
cd webui && npm install && npm run dev # listens on :5173
|
||||
```
|
||||
|
||||
After code changes:
|
||||
|
||||
```bash
|
||||
make build-api # rebuild only the API container
|
||||
make build-webui # rebuild only the webui container
|
||||
make restart # restart containers without rebuild
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key conventions
|
||||
|
||||
- All container lifecycle goes through `make` targets. Never call `docker` or `docker compose` directly in development.
|
||||
- All API calls from the UI go through `webui/src/services/api.js`. Never add a new Axios instance or raw `fetch` call.
|
||||
- Business logic belongs in manager classes, not in Flask route handlers. Route handlers should validate input, call the manager, and return JSON.
|
||||
- Config reads always go through `ConfigManager`. Never open `cell_config.json` directly.
|
||||
- Run `make test` before committing. The pre-commit hook blocks commits that fail tests.
|
||||
|
||||
---
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
api/ Flask API and all service managers
|
||||
webui/ React SPA (Vite + Tailwind)
|
||||
tests/ pytest unit tests
|
||||
tests/integration/ require a running PIC stack
|
||||
tests/e2e/ Playwright UI and WireGuard tests
|
||||
config/ Runtime config (mostly git-ignored)
|
||||
data/ Runtime secrets and state (fully git-ignored)
|
||||
scripts/ Setup and maintenance scripts
|
||||
docs/ Detailed developer documentation
|
||||
install.sh One-line installer
|
||||
Makefile All operational commands
|
||||
docker-compose.yml
|
||||
```
|
||||
@@ -0,0 +1,159 @@
|
||||
# Install Internals
|
||||
|
||||
This page describes what the installer does, the make targets, the systemd unit, and debugging options.
|
||||
|
||||
---
|
||||
|
||||
## install.sh
|
||||
|
||||
The installer at `https://install.pic.ngo` is a Bash script (`install.sh` in the repository root). It is served from the DDNS VPS.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Effect |
|
||||
|---|---|
|
||||
| `--debug` | Verbose output to terminal; stdout/stderr are tee'd to both the log file and the terminal |
|
||||
| `--force` | Bypass the idempotency check (`.installed` file) |
|
||||
| `PIC_DIR=/path` | Override install directory (default `/opt/pic`) |
|
||||
| `PIC_DEBUG=1` | Same as `--debug`, usable as an env var |
|
||||
|
||||
The install log is always written to `/var/log/pic-install.log` (or `/tmp/pic-install.log` if `/var/log` is not writable).
|
||||
|
||||
### Seven steps
|
||||
|
||||
| Step | What happens |
|
||||
|---|---|
|
||||
| 1 | Detect OS from `/etc/os-release`; select `apt`, `dnf`, or `apk` |
|
||||
| 2 | Install Docker, git, make; install and start host chrony |
|
||||
| 3 | Create `pic` system user; add to `docker` group |
|
||||
| 4 | Clone repository to `${PIC_DIR}` (or `git pull` if already cloned) |
|
||||
| 5 | Run `make install` — generates keys and config, writes systemd unit; scan stdout for the admin password banner and relay it to the terminal |
|
||||
| 6 | Run `make start-core` to bring up the six core containers |
|
||||
| 7 | Enable and start the `pic` systemd unit; poll `http://127.0.0.1:3000/health` for up to 60 seconds |
|
||||
|
||||
After step 7, the installer prints the browser URL for the setup wizard.
|
||||
|
||||
### Idempotency
|
||||
|
||||
If `${PIC_DIR}/.installed` exists and `--force` is not passed, the installer exits with a message to run `make update` instead.
|
||||
|
||||
---
|
||||
|
||||
## make targets
|
||||
|
||||
All container operations go through `make`. The Makefile is at the project root.
|
||||
|
||||
### Stack lifecycle
|
||||
|
||||
```bash
|
||||
make start # docker compose up -d --build (full profile)
|
||||
make start-core # bring up only the six core containers
|
||||
make stop # docker compose down
|
||||
make restart # docker compose restart
|
||||
make status # container status + API /health check
|
||||
make logs # follow all service logs
|
||||
make logs-<svc> # e.g. make logs-api, make logs-wireguard
|
||||
make shell-<svc> # shell inside container, e.g. make shell-api
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
make build-api # rebuild cell-api image after code change
|
||||
make build-webui # rebuild cell-webui image after code change
|
||||
```
|
||||
|
||||
### First install and maintenance
|
||||
|
||||
```bash
|
||||
make install # generate keys, config, systemd unit; write .installed
|
||||
make update # git pull + rebuild + restart
|
||||
make reinstall # full wipe of config/ and data/, then setup + start
|
||||
make uninstall # stop containers; prompt to delete config/ and data/
|
||||
```
|
||||
|
||||
### Operations
|
||||
|
||||
```bash
|
||||
make backup # tar config/ + data/ into backups/cell-backup-<timestamp>.tar.gz
|
||||
make restore # list available backup archives
|
||||
make list-peers # show peers via API
|
||||
make show-routes # wg show inside cell-wireguard
|
||||
make show-admin-password # print current admin password
|
||||
make reset-admin-password # generate and set a new random admin password
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
make test # pytest unit suite
|
||||
make test-coverage # with coverage; htmlcov/
|
||||
make test-api # API endpoint tests only
|
||||
make test-integration # full integration tests (running stack required)
|
||||
make test-integration-readonly # read-only integration tests
|
||||
make test-e2e-deps # install Playwright (run once)
|
||||
make test-e2e-api # API e2e tests
|
||||
make test-e2e-ui # UI e2e tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## systemd unit
|
||||
|
||||
`make install` copies `scripts/pic.service` to `/etc/systemd/system/pic.service` and runs `systemctl daemon-reload && systemctl enable pic`. The unit starts and stops the PIC stack on boot and shutdown. The installer also calls `systemctl start pic` after `make start-core` succeeds.
|
||||
|
||||
`make uninstall` calls `systemctl disable --now pic`, removes the unit file, and runs `systemctl daemon-reload`.
|
||||
|
||||
On Alpine Linux (OpenRC), the installer skips systemd and uses OpenRC's `rc-update` instead.
|
||||
|
||||
---
|
||||
|
||||
## Host NTP
|
||||
|
||||
The installer installs and enables `chrony` on the host (step 2). This is not the same as the `cell-ntp` container — it is the host's own time synchronisation, needed for:
|
||||
|
||||
- ACME certificate issuance (Let's Encrypt rejects challenges with skewed clocks)
|
||||
- `pic.ngo` DDNS registration (the registration endpoint requires a valid TOTP derived from the host clock)
|
||||
|
||||
If the installer cannot start chrony, it prints a warning. Proceed only after verifying the clock is accurate (`date`).
|
||||
|
||||
---
|
||||
|
||||
## Debugging an install
|
||||
|
||||
To see the full verbose output of every installer step:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.pic.ngo | PIC_DEBUG=1 sudo -E bash
|
||||
```
|
||||
|
||||
Or with the flag:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.pic.ngo | sudo bash -s -- --debug
|
||||
```
|
||||
|
||||
The complete log (including all subprocess output) is always at `/var/log/pic-install.log` regardless of debug mode.
|
||||
|
||||
To debug a failing step after the installer exits:
|
||||
|
||||
```bash
|
||||
sudo cat /var/log/pic-install.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `.env` overrides
|
||||
|
||||
A `.env` file in the project root overrides container IPs and port assignments. A `.env` file is not required — all variables have defaults. Create one only if you need to change ports or move containers to different IPs.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `CELL_NETWORK` | `172.20.0.0/16` | Docker bridge subnet |
|
||||
| `DNS_PORT` | `53` | DNS (UDP + TCP) |
|
||||
| `NTP_PORT` | `123` | NTP (UDP) |
|
||||
| `WG_PORT` | `51820` | WireGuard listen port (UDP) |
|
||||
| `API_PORT` | `3000` | Flask API (127.0.0.1 only) |
|
||||
| `WEBUI_PORT` | `8081` | Host port for the web UI |
|
||||
| `FLASK_DEBUG` | (unset) | Set to `1` for Flask debug mode; do not use in production |
|
||||
| `PUID` / `PGID` | current user | UID/GID for the WireGuard container |
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
# Testing
|
||||
|
||||
---
|
||||
|
||||
## Running tests
|
||||
|
||||
```bash
|
||||
make test # pytest unit tests (excludes e2e and integration)
|
||||
make test-coverage # with coverage; HTML report in htmlcov/
|
||||
make test-api # API endpoint tests only
|
||||
```
|
||||
|
||||
All unit tests pass without a running stack. CI (Gitea Actions) runs the unit suite on every push.
|
||||
|
||||
---
|
||||
|
||||
## Test layout
|
||||
|
||||
```
|
||||
tests/ Unit and API endpoint tests; no services required
|
||||
tests/integration/ Require a running PIC stack
|
||||
tests/e2e/ Playwright UI and WireGuard end-to-end tests
|
||||
```
|
||||
|
||||
### Unit tests
|
||||
|
||||
The unit suite covers manager classes, route handlers, and utility functions. Tests use `unittest.mock` / `pytest-mock` for all Docker SDK calls, filesystem writes, and subprocess invocations.
|
||||
|
||||
Important rule: when testing write-failure paths, mock `builtins.open` with `side_effect=OSError`. Do not rely on unwritable host paths — CI may run as root and can create any path.
|
||||
|
||||
The test count grows as features are added. CLAUDE.md references "1500+" tests; the README references "3170 tests" in older documentation. Run `make test` to see the current count.
|
||||
|
||||
### Integration tests
|
||||
|
||||
```bash
|
||||
make test-integration # full suite; modifies cell state
|
||||
make test-integration-readonly # read-only checks; safe to run at any time
|
||||
```
|
||||
|
||||
Require a running `make start` stack. These tests create peers, install services, and change configuration. Run them against a development cell, not production.
|
||||
|
||||
### End-to-end tests
|
||||
|
||||
```bash
|
||||
make test-e2e-deps # install Playwright and browser binaries (run once)
|
||||
make test-e2e-api # API-level e2e tests
|
||||
make test-e2e-ui # Browser-level UI tests via Playwright
|
||||
```
|
||||
|
||||
The Playwright tests drive a real browser against a running stack. `test-e2e-ui` tests include WireGuard tunnel tests that require actual WireGuard kernel support.
|
||||
|
||||
---
|
||||
|
||||
## What to test
|
||||
|
||||
- Every new API endpoint needs at least one test: happy path, auth failure (401), and the expected error case.
|
||||
- Every new manager method needs a test that mocks its external dependencies (Docker, filesystem, subprocess).
|
||||
- Do not test Flask routing boilerplate or trivial getters. Test behaviour and invariants.
|
||||
- Do not add tests for things that are covered clearly by the code itself — only add tests when the behaviour is non-obvious or the failure mode matters.
|
||||
|
||||
---
|
||||
|
||||
## CI
|
||||
|
||||
Gitea Actions runs the unit suite on every push using a self-hosted runner on `pic0` (192.168.31.51). The workflow file calls:
|
||||
|
||||
```bash
|
||||
pytest tests/ --ignore=tests/e2e --ignore=tests/integration
|
||||
```
|
||||
|
||||
Image builds (for the email, calendar, and files service images) run on path-filtered pushes to the `pic-services` repository.
|
||||
|
||||
---
|
||||
|
||||
## Webui tests
|
||||
|
||||
```bash
|
||||
cd webui && npm run test # vitest unit tests
|
||||
```
|
||||
|
||||
The webui unit tests cover React component logic and the Axios client wrapper. They do not require a running API.
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# Personal Internet Cell
|
||||
|
||||
A **Personal Internet Cell** (PIC) is a self-hosted server you run on your own hardware. It gives you a private VPN, DNS, NTP, and HTTPS reverse proxy — and optionally email, calendar, contacts, and file storage — all managed from a single web interface.
|
||||
|
||||
One command installs everything:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.pic.ngo | sudo bash
|
||||
```
|
||||
|
||||
When the installer finishes, it prints a URL. Open it in a browser and a setup wizard walks you through the rest.
|
||||
|
||||
---
|
||||
|
||||
## Who is this wiki for?
|
||||
|
||||
| I am... | Start here |
|
||||
|---|---|
|
||||
| New to self-hosting and want to know what PIC is | [Noobs — Start Here](Noobs-Start-Here) |
|
||||
| A cell member who needs to connect or use services | [User Guide](User-Guide) |
|
||||
| The person running a cell (the admin) | [Admin Guide](Admin-Guide) |
|
||||
| A developer contributing to PIC or building a service | [Dev Guide](Dev-Guide) |
|
||||
|
||||
---
|
||||
|
||||
## What you get
|
||||
|
||||
- WireGuard VPN — your devices connect securely to your cell
|
||||
- Private DNS and NTP for connected devices
|
||||
- Automatic HTTPS certificates (Let's Encrypt or internal CA)
|
||||
- Optional services installed from the built-in store: email, calendar/contacts, file storage, and connectivity exits
|
||||
- A browser-based dashboard for managing everything — no SSH required for day-to-day use
|
||||
@@ -0,0 +1,94 @@
|
||||
# Noobs — Start Here
|
||||
|
||||
## What is a Personal Internet Cell?
|
||||
|
||||
A Personal Internet Cell is a small server you run at home (or on a cheap VPS). It does the things that big tech companies normally do for you — but on your own hardware, under your control.
|
||||
|
||||
Concretely, it gives you:
|
||||
|
||||
- **A private VPN.** Your phone, laptop, and other devices connect to your cell over an encrypted WireGuard tunnel. Traffic you want to route through your home network goes through your cell instead of a stranger's server.
|
||||
- **Private DNS.** Your connected devices use your cell's DNS resolver. Nobody else sees your queries.
|
||||
- **Accurate time (NTP).** Your devices synchronise their clocks from your cell.
|
||||
- **HTTPS.** Every service your cell runs gets a real TLS certificate automatically.
|
||||
- **Optional services.** You can install email, calendar/contacts, or file storage from the built-in service store. These run on your cell — your data does not leave your hardware.
|
||||
|
||||
---
|
||||
|
||||
## Do I need to be technical?
|
||||
|
||||
You need to be comfortable running a Linux server and following instructions. You do not need to edit config files by hand or write any code. Everything happens through a browser-based interface after the initial install.
|
||||
|
||||
---
|
||||
|
||||
## What hardware do I need?
|
||||
|
||||
Any always-on Linux machine with:
|
||||
|
||||
- 2 GB+ RAM
|
||||
- 10 GB+ disk
|
||||
- A network connection
|
||||
|
||||
A Raspberry Pi 4, an old laptop, a NUC, or a cheap VPS all work. The supported Linux distributions are Debian, Ubuntu, Fedora, RHEL, and Alpine.
|
||||
|
||||
---
|
||||
|
||||
## How do I install it?
|
||||
|
||||
Run this single command on your Linux machine:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.pic.ngo | sudo bash
|
||||
```
|
||||
|
||||
Before running any script piped through `sudo bash`, you can review it first:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://install.pic.ngo | less
|
||||
```
|
||||
|
||||
The installer runs 7 steps. It detects your OS, installs Docker and the other dependencies, creates a `pic` system user, clones the code to `/opt/pic`, generates the initial configuration, starts the core services, and waits for everything to be healthy. The whole process takes a few minutes on a typical internet connection.
|
||||
|
||||
When it finishes, it prints something like:
|
||||
|
||||
```
|
||||
Open your browser: http://192.168.1.10:8081/setup
|
||||
```
|
||||
|
||||
Open that URL.
|
||||
|
||||
---
|
||||
|
||||
## The setup wizard
|
||||
|
||||
A browser-based wizard appears automatically. It asks you four things:
|
||||
|
||||
1. **Cell name** — a short name like `myhome` or `alices-cell`. This becomes part of your domain name if you use the `pic.ngo` option.
|
||||
2. **Domain mode** — how your cell gets an HTTPS certificate. If you are not sure, choose `pic.ngo` for internet-facing use, or `lan` for a local-only setup.
|
||||
3. **Timezone** — your local timezone.
|
||||
4. **Admin password** — at least 12 characters, with uppercase, lowercase, and a digit.
|
||||
|
||||
Click **Complete Setup**. The wizard creates your admin account and redirects you to the login page.
|
||||
|
||||
Log in with username `admin` and the password you just set.
|
||||
|
||||
---
|
||||
|
||||
## What now?
|
||||
|
||||
You are in the dashboard. From here you can:
|
||||
|
||||
- Go to **Peers** to add your devices to the VPN — see [Connect to the VPN](User-Connect-VPN).
|
||||
- Go to **Services** to install email, calendar, or file storage.
|
||||
- Read the [Admin Guide](Admin-Guide) for everything else.
|
||||
|
||||
---
|
||||
|
||||
## What does "split-horizon DNS" mean?
|
||||
|
||||
When you connect to your cell over VPN, your domain name (e.g. `myhome.pic.ngo`) resolves to your cell's VPN address. Traffic stays inside the tunnel. When you are off the VPN, the same name resolves to your cell's public IP. You do not need to do anything — it works automatically.
|
||||
|
||||
---
|
||||
|
||||
## I got stuck
|
||||
|
||||
See [Monitoring and Troubleshooting](Admin-Monitoring-Troubleshooting) for common problems and fixes.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Connecting to the VPN
|
||||
|
||||
Your cell admin creates a WireGuard peer configuration for you. This page explains how to get it and use it.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Get your configuration from the admin
|
||||
|
||||
Ask your admin to add you as a peer. They will give you either:
|
||||
|
||||
- A **QR code** — easiest on a phone
|
||||
- A **configuration file** (`.conf`) — easiest on a desktop or laptop
|
||||
|
||||
The admin generates these from the **Peers** page in the cell dashboard. The configuration contains a private key that was generated specifically for you. Treat it like a password.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Install the WireGuard app
|
||||
|
||||
Install the WireGuard client for your device:
|
||||
|
||||
- **Android / iOS** — install the WireGuard app from the Play Store or App Store
|
||||
- **macOS** — install WireGuard from the Mac App Store
|
||||
- **Windows** — download the installer from [wireguard.com](https://www.wireguard.com/install/)
|
||||
- **Linux** — install the `wireguard-tools` package for your distribution
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Import the configuration
|
||||
|
||||
**On a phone (QR code):**
|
||||
|
||||
1. Open the WireGuard app.
|
||||
2. Tap the `+` button.
|
||||
3. Choose **Scan from QR code**.
|
||||
4. Point your camera at the QR code your admin shows you.
|
||||
5. Give the tunnel a name.
|
||||
|
||||
**On a desktop (config file):**
|
||||
|
||||
1. Open the WireGuard app.
|
||||
2. Click **Import tunnel(s) from file** (or `Add Tunnel` on Linux).
|
||||
3. Select the `.conf` file.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Connect
|
||||
|
||||
Toggle the tunnel on. After a few seconds, the status should show **Active**.
|
||||
|
||||
---
|
||||
|
||||
## Verifying it works
|
||||
|
||||
Once connected:
|
||||
|
||||
- Open a browser and navigate to your cell's domain (your admin will tell you the address, e.g. `https://myhome.pic.ngo`). It should load over HTTPS.
|
||||
- DNS names ending in `.cell` (like `api.cell`) should resolve from inside the VPN — these are internal names that only work when connected.
|
||||
|
||||
If the domain works on the VPN but not off it, that is expected — some services are only available inside the tunnel.
|
||||
|
||||
---
|
||||
|
||||
## What is split-horizon DNS?
|
||||
|
||||
PIC runs a split-horizon DNS configuration. This means:
|
||||
|
||||
- **Inside the VPN:** your cell's domain name (e.g. `myhome.pic.ngo`) resolves to the cell's internal WireGuard address. Traffic goes through the tunnel. This is more efficient and keeps traffic private.
|
||||
- **Outside the VPN:** the same domain resolves to the cell's public IP. Traffic goes over the regular internet.
|
||||
|
||||
You do not need to configure anything for this to work. It is automatic.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**The tunnel activates but nothing loads.**
|
||||
Check that the cell is running. Ask your admin to run `make status`.
|
||||
|
||||
**"Handshake did not complete" or no traffic.**
|
||||
Make sure the cell's WireGuard port (default `51820/udp`) is reachable from your network. If you are behind a strict firewall, ask your admin whether an alternate connectivity method is configured.
|
||||
|
||||
**The cell's domain resolves but HTTPS fails.**
|
||||
The TLS certificate may still be provisioning. Wait a few minutes and try again.
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# User Guide
|
||||
|
||||
This guide is for people who have been invited to connect to someone else's Personal Internet Cell. You are a **peer** — not the admin.
|
||||
|
||||
Your admin is the person who set up and runs the cell. Ask them for your WireGuard configuration file or QR code before following this guide.
|
||||
|
||||
---
|
||||
|
||||
## What you can do as a peer
|
||||
|
||||
- Connect to the cell's VPN and route traffic through it
|
||||
- Use the cell's private DNS and NTP
|
||||
- Use optional services the admin has installed: email, calendar/contacts, file storage
|
||||
- View your own connection info from the peer dashboard
|
||||
|
||||
---
|
||||
|
||||
## Sections
|
||||
|
||||
- [Connect to the VPN](User-Connect-VPN) — get your config, install WireGuard, connect
|
||||
- [Using Services](User-Services) — email, calendar, files, and what split-horizon DNS means in practice
|
||||
|
||||
---
|
||||
|
||||
## The peer dashboard
|
||||
|
||||
Once connected, you can open the cell's web address in a browser. Log in with the credentials your admin gives you. The peer dashboard shows:
|
||||
|
||||
- Your VPN connection status
|
||||
- Connection info for any services the admin has set up for you (server addresses, ports, your username)
|
||||
|
||||
The admin controls page and settings are not visible to peers.
|
||||
|
||||
---
|
||||
|
||||
## Getting help
|
||||
|
||||
If something is not working, contact your cell admin. They can check your peer config, reset your password, or check the service status.
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# Using Services
|
||||
|
||||
Optional services — email, calendar/contacts, and file storage — are installed by the cell admin. Not every cell will have all of them. Ask your admin what is available.
|
||||
|
||||
---
|
||||
|
||||
## Finding your connection details
|
||||
|
||||
Log in to the cell dashboard as a peer. The peer dashboard shows the connection information for every service your admin has set up for you: server hostnames, ports, your username, and your password.
|
||||
|
||||
You can also call this endpoint directly once connected:
|
||||
|
||||
```
|
||||
GET /api/peers/<your-username>/service-credentials
|
||||
```
|
||||
|
||||
This returns a filled-in connection info block for each service.
|
||||
|
||||
---
|
||||
|
||||
## Email
|
||||
|
||||
If email is installed, you can use any standard email client (Thunderbird, Apple Mail, K-9 Mail, etc.).
|
||||
|
||||
Your admin will give you:
|
||||
|
||||
- **IMAP server** — for receiving mail
|
||||
- **SMTP server** — for sending mail
|
||||
- **Username** — typically `you@yourdomain`
|
||||
- **Password** — set by the admin
|
||||
|
||||
Standard ports:
|
||||
|
||||
| Protocol | Port | Security |
|
||||
|---|---|---|
|
||||
| IMAP | 993 | SSL/TLS |
|
||||
| SMTP (submission) | 587 | STARTTLS |
|
||||
|
||||
Use your cell's domain as the server address. Example: if your cell is `myhome.pic.ngo`, the IMAP server is `mail.myhome.pic.ngo`.
|
||||
|
||||
---
|
||||
|
||||
## Calendar and contacts (CalDAV / CardDAV)
|
||||
|
||||
If the calendar service is installed, you can use it with any CalDAV/CardDAV client (Apple Calendar, Thunderbird + Lightning, DAVx5 on Android, etc.).
|
||||
|
||||
| Protocol | URL pattern |
|
||||
|---|---|
|
||||
| CalDAV | `https://calendar.<cell-domain>/` |
|
||||
| CardDAV | `https://calendar.<cell-domain>/` |
|
||||
|
||||
Your username and password are the same as your cell login credentials for this service.
|
||||
|
||||
DAVx5 (Android) auto-discovery tip: enter `https://calendar.<cell-domain>/` as the base URL and DAVx5 will find your calendars and address books automatically.
|
||||
|
||||
---
|
||||
|
||||
## File storage (WebDAV)
|
||||
|
||||
If the files service is installed, you can mount it as a network drive or use a WebDAV client (Cyberduck, Finder on macOS, any WebDAV-capable app).
|
||||
|
||||
| URL | `https://files.<cell-domain>/dav/<your-username>/` |
|
||||
|---|---|
|
||||
|
||||
Your username and password come from your peer connection info.
|
||||
|
||||
A browser-based file manager may also be available at `https://files.<cell-domain>/` — ask your admin.
|
||||
|
||||
---
|
||||
|
||||
## A note about services and the VPN
|
||||
|
||||
Services are available both inside and outside the VPN (traffic routing depends on your domain mode and the admin's configuration). If a service does not load off the VPN, try connecting to the VPN first.
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
## Personal Internet Cell
|
||||
|
||||
[Home](Home)
|
||||
|
||||
---
|
||||
|
||||
### New to PIC?
|
||||
|
||||
[Start Here (Noobs)](Noobs-Start-Here)
|
||||
|
||||
---
|
||||
|
||||
### User
|
||||
|
||||
[User Guide](User-Guide)
|
||||
[Connect to the VPN](User-Connect-VPN)
|
||||
[Using Services](User-Services)
|
||||
|
||||
---
|
||||
|
||||
### Admin
|
||||
|
||||
[Admin Guide](Admin-Guide)
|
||||
[Setup and First Run](Admin-Setup)
|
||||
[Domains and TLS](Admin-Domains-TLS)
|
||||
[Services Catalog](Admin-Services)
|
||||
[Connectivity](Admin-Connectivity)
|
||||
[Peers](Admin-Peers)
|
||||
[Backup and Restore](Admin-Backup)
|
||||
[Logging and Audit](Admin-Logging-Audit)
|
||||
[Monitoring and Troubleshooting](Admin-Monitoring-Troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
### Developer
|
||||
|
||||
[Dev Guide](Dev-Guide)
|
||||
[Architecture](Dev-Architecture)
|
||||
[Building a Service](Dev-Building-Services)
|
||||
[API Reference](Dev-API)
|
||||
[Testing](Dev-Testing)
|
||||
[Install Internals](Dev-Install-Internals)
|
||||
Reference in New Issue
Block a user