amnezia-fellow
Amnezia VPN client manager written in Rust on top of the public cot framework.
The app uses SQLite or PostgreSQL as the source of truth, authenticates users through OIDC/SSO, renders AmneziaWG client peers into a Kubernetes Secret, and avoids updating that Secret when the rendered content is byte-for-byte identical.
Quick Start
export AMNEZIA_FELLOW_DATABASE_URL=sqlite://amnezia-fellow.sqlite3?mode=rwc
cargo run -- --listen 127.0.0.1:8000
Open http://localhost:8000/admin/setup to create the first local admin account.
Docker
The image is built by GitHub Actions and published to Docker Hub as
ultradesu/amnezia-fellow.
Required repository secrets:
DOCKERHUB_USERNAMEDOCKERHUB_TOKEN
Publishing runs for version tags matching v*.*.*. A tag like v0.1.0 pushes:
ultradesu/amnezia-fellow:v0.1.0ultradesu/amnezia-fellow:0.1.0ultradesu/amnezia-fellow:latest
Local image build:
docker build -t amnezia-fellow .
docker run --rm -p 8000:8000 -v "$PWD/data:/data" amnezia-fellow
Roles
There are two roles:
admin: full access, sees all client configs.client: sees and manages only their own configs.
OIDC provisioning is deny-by-default:
- users in
AMNEZIA_FELLOW_OIDC_ADMIN_GROUPSbecomeadmin; - users in
AMNEZIA_FELLOW_OIDC_CLIENT_GROUPSbecomeclient; - users outside both group lists cannot sign in.
The OIDC groups claim is expected to be groups.
VPN Data Model
The database stores all client data needed to restore configs:
- owner user id
- display name
- assigned IPv4 address
- public key
- private key
- enabled flag
- created/updated timestamps
The Kubernetes Secret is derived from the database. Active clients are rendered into one configured Secret key, peers.conf by default.
Kubernetes Sync
The app is intended to run inside Kubernetes with a ServiceAccount and RBAC that can read and update Secrets and list Pods in the Amnezia namespace.
It reads:
- server public key from
AMNEZIA_FELLOW_K8S_SERVER_SECRET, keyserver-public-key; - node endpoints from
AMNEZIA_FELLOW_K8S_ENDPOINTS_SECRET; Secret data keys are used as Amnezia server names in generatedvpn://links; - AmneziaWG pods selected by
app=amneziawgfor config rollout status; - writes rendered client peers to
AMNEZIA_FELLOW_K8S_CLIENTS_SECRET.
It does not modify the server Secret. Argo/ExternalSecrets can keep managing amneziawg-server, while amnezia-fellow owns only the client Secret content.
UI
The main user interface is /configs. It is an Alpine.js reactive page backed by JSON API endpoints:
- clients and admins use the same page;
- the backend filters data by role;
- admins get an extra manual Secret sync action.
- clients and admins see AmneziaWG pod rollout status and uptime.
Environment Variables
All settings use AMNEZIA_FELLOW_ prefix. Priority is:
environment variable > database override > compiled default
| Variable | Description | Default |
|---|---|---|
AMNEZIA_FELLOW_DATABASE_URL |
SQLite or PostgreSQL connection URL. PostgreSQL URLs must start with postgresql://. |
sqlite://amnezia-fellow.sqlite3?mode=rwc |
AMNEZIA_FELLOW_MIGRATE_SQLITE |
Optional SQLite path/URL imported into PostgreSQL once on startup | empty |
AMNEZIA_FELLOW_LOG_LEVEL |
Tracing filter | info |
AMNEZIA_FELLOW_AUTH_PASSWORD_ENABLED |
Enable password login | true |
AMNEZIA_FELLOW_AUTH_SSO_ENABLED |
Enable OIDC login | false |
AMNEZIA_FELLOW_OIDC_ISSUER |
OIDC issuer URL | empty |
AMNEZIA_FELLOW_OIDC_CLIENT_ID |
OIDC client ID | empty |
AMNEZIA_FELLOW_OIDC_CLIENT_SECRET |
OIDC client secret | empty |
AMNEZIA_FELLOW_OIDC_BUTTON_TEXT |
SSO button label | Sign in with SSO |
AMNEZIA_FELLOW_OIDC_ADMIN_GROUPS |
Comma-separated admin groups | empty |
AMNEZIA_FELLOW_OIDC_CLIENT_GROUPS |
Comma-separated client groups | empty |
AMNEZIA_FELLOW_K8S_NAMESPACE |
Amnezia namespace | amnezia |
AMNEZIA_FELLOW_K8S_CLIENTS_SECRET |
Client peers Secret | amneziawg-clients |
AMNEZIA_FELLOW_K8S_CLIENTS_SECRET_KEY |
Secret data key for rendered peers | peers.conf |
AMNEZIA_FELLOW_K8S_SERVER_SECRET |
Server config Secret | amneziawg-server |
AMNEZIA_FELLOW_K8S_ENDPOINTS_SECRET |
Node endpoints Secret | amneziawg-endpoints |
AMNEZIA_FELLOW_VPN_CLIENT_CIDR |
Client address pool | 10.8.0.0/16 |
AMNEZIA_FELLOW_VPN_DNS |
DNS servers in generated configs | 1.1.1.1, 8.8.8.8 |
AMNEZIA_FELLOW_VPN_MTU |
MTU in generated configs | 1376 |
AMNEZIA_FELLOW_SWAGGER_ENABLED |
Serve Swagger UI at /swagger/ |
false |
AMNEZIA_FELLOW_TELEGRAM_BOT_ENABLED |
Enable Telegram bot/Web App integration | false |
AMNEZIA_FELLOW_TELEGRAM_BOT_USERNAME |
Telegram bot username, with or without @ |
empty |
AMNEZIA_FELLOW_TELEGRAM_BOT_TOKEN |
Telegram bot token used to verify Web App initData |
empty |
PostgreSQL Migration
To move from SQLite to PostgreSQL, start the app with a PostgreSQL database URL
and point AMNEZIA_FELLOW_MIGRATE_SQLITE at the existing SQLite file:
AMNEZIA_FELLOW_DATABASE_URL=postgresql://user:pass@postgres:5432/amnezia_fellow
AMNEZIA_FELLOW_MIGRATE_SQLITE=/data/amnezia-fellow.sqlite3
On startup, the app runs its migrations on PostgreSQL, opens the SQLite source read/write, applies any missing app migrations there, copies config entries, database sessions, users, OIDC links, Telegram link state, and VPN clients, then writes an import marker into PostgreSQL. If the marker already exists, the import is skipped. If PostgreSQL already contains app data but has no marker, startup fails instead of merging two databases implicitly.
Telegram Bot
Configure the bot through the admin Settings page or the matching environment variables:
AMNEZIA_FELLOW_TELEGRAM_BOT_ENABLED=true
AMNEZIA_FELLOW_TELEGRAM_BOT_USERNAME=<BOT_USERNAME>
AMNEZIA_FELLOW_TELEGRAM_BOT_TOKEN=<BOT_TOKEN>
In BotFather, create or reuse the same bot and set its Web App URL to
https://<APP_HOST>/configs. This app runs the bot in the same process with
getUpdates polling, so do not configure a separate webhook for the same bot.
For browser-based linking, users copy a one-time secret code from the portal and
send it to this bot; the app reads the Telegram sender ID from that message.
Users must open the bot and send /start once before notifications can be
delivered.
API
The JSON API is session-authenticated:
GET /api/meGET /api/vpn-clientsGET /api/vpn-statusGET /api/telegram-link/statusPOST /api/telegram-link/webappPOST /api/telegram-link/startPOST /api/telegram-link/declinePOST /api/telegram-link/unlinkPOST /api/vpn-clientsPOST /api/vpn-clients/{id}/enabledDELETE /api/vpn-clients/{id}GET /api/vpn-clients/{id}/configreturnsservers[]with one raw AWG config and one Amneziavpn://import link per registered endpointPOST /api/vpn-clients/sync
Development
cargo fmt
cargo check
The project depends on public crates only; cot = "0.6.0" is pulled from crates.io.