Layers
| Layer | Mechanism |
|---|---|
| Node firewall | nftables from service declarations; default drop |
| Tenant network | Outbound policy matched by uid |
| Abuse | Kernel flood limits + CrowdSec, bans shared |
| People | 3 roles, Argon2id, TOTP, CSRF, scoped tokens |
| Nodes | Cluster CA, mutual TLS, route allowlist |
| Secrets | AES-256-GCM, reveals audited |
| Change | Append-only audit log, signed releases |
Firewall
Each module declares port, protocol, scope. The ruleset (inet ozpanel_fw) is derived. Scopes: public · peers · admin (management sources; open while none) · local.
| Call | Does |
|---|---|
GET /nftables/plan, /nftables/enforcement | Implied ruleset; per port: listening, reach, as declared |
GET/POST/DELETE /nftables/admin-sources | Management sources; scope all | console; says if you are covered |
GET /nftables/audit, POST {content} | Lockout and exposure check |
POST /nftables/baseline/apply, /nftables/apply | Apply; 20 s auto-revert unless POST /nftables/confirm {token} |
POST /nftables/persist, /reload, PUT /nftables/peers | Persist; reload; resync peer sets |
Flood limit (/nftables/settings) | Default |
|---|---|
conn_per_ip_smtp | 20 |
conn_per_ip_mail_clients | 100 |
syn_per_ip_per_sec | 100 |
icmp_echo_per_sec | 20 |
admin_console_restricted | false |
Ports
| Scope | Ports |
|---|---|
| public | 80, 443 tcp+udp, 53, 25, 587, 465, 143, 993, 110, 995 |
| admin | 22, 9600 |
| peers | 9601, 656, 3306 (+ container bridge), 4567, 4568, 4444, 4569, 5432, 6379, 26379, 26380, 3901 |
| local | 6432, 2019, 8081, 8080, 6060, 11332–11334, 10025, 10041, 4190, 24, 20000, 11211, 3900, 3903, 7680–7699 |
| retired (closed) | 24007 and bricks, 55555/udp (GlusterFS) |
Tenant network policy
Table inet ozpanel_tenants, matched on socket uid; reject, not drop. Plan value, tenant override.
| Field | Default |
|---|---|
outbound_default | allow |
deny_ports | 25, 465, 587 |
deny_cidrs | 169.254/16, 10/8, 172.16/12, 192.168/16, 100.64/10 |
allow_ports, allow_cidrs | empty (used when default is deny) |
listen_ports | empty |
Guard: every non-public declared port closed to all tenant uids, except 3306, 6432, 6379, 10025. /network-policy, …/tenants/{id}, …/plans/{id}, POST …/apply. /proc mounted hidepid=invisible.
CrowdSec
Engine, local API and firewall bouncer per node; decisions in PostgreSQL, shared. Bans stay in the kernel if the database is down. Hub auto-upgrade off.
Setting (/crowdsec/settings) | Default |
|---|---|
smtp_auth_fail_count / _window_min | 5 / 10 |
silent_conn_count / _window_min | 10 / 10 |
ban_base_hours / ban_max_hours | 4 (doubles) / 168 |
trusted_login_hours | 24 |
| Rule auto-upgrade | off |
log_level, log_media, compress_logs, prometheus_* | info, file, true, full |
Never banned: management sources, cluster nodes, recent successful logins. GET /crowdsec/decisions · POST …/decisions/add {"ip","duration","reason"} · …/delete.
SSH
GET/PUT /ssh/settings; drop-in, sshd -t, reload; sessions kept.
| Key | Default |
|---|---|
port (per node) | 22 |
permitrootlogin | yes (install and join use it) |
passwordauthentication, pubkeyauthentication | yes, yes |
maxauthtries, logingracetime | 6, 120 |
x11forwarding, allowtcpforwarding | yes, yes |
clientaliveinterval, clientalivecountmax | 0, 3 |
Restrict with management sources first, confirm keys, then tighten.
Secrets
- Stores
panel_secrets(replicated, AES-256-GCM underapp_key) ·/etc/ozpanel/secrets/files. - Calls
GET/POST /secrets,GET/DELETE /secrets/{name},GET /secrets/files,POST /secrets/files/{name}/reveal(audited). - App key
POST /secrets/app-key/rotate: secret store on this node. Cluster-wide coordinated rotation: not available. - Cluster secrets
PUT /cluster/secrets/{name},…/rollout,POST /cluster/secrets/pull. Service rotations: Redis ACL, PgBouncer, MariaDB, webmail DB.
Logins and sessions
| Item | Value |
|---|---|
| Password hash | Argon2id, 64 MiB, 3 iterations, parallelism 2 |
| Minimum length | 10 |
| Lockout | 5 failures → 15 min; 20 per IP per 15 min → 429 |
| 2FA | TOTP, optional; 10 single-use recovery codes |
| Session | ozpanel_session, HttpOnly, SameSite=Lax; 12 h sliding (mailbox 30 days); CSRF header |
| Reset / invite | 1 h / 7 days, single use; 3 per address per hour |
| Admin restriction | admin_console_restricted: super_admin sessions and tokens only from management sources. Break-glass: ozpanel admin console-restriction off. |
| Recovery token | /etc/ozpanel/secrets/recovery_token; recovery routes only |
| Recovery grants | ozpanel admin recovery-grant --node N --allow diagnose|recover|force |
API tokens: see API · Tokens.
Audit log
Every mutating call, refusals and sign-ins: principal, role, tenant_id, module, action, params (≤1 MiB, unredacted), ok, ip, node. Append-only. GET /ozpanel/audit?module=&action=&principal=&since=&until= · /ozpanel/audit/export (HMAC-SHA256 signed).
Cluster key and releases
- CA key
/etc/ozpanel/pki/cluster-ca.key, 0600. On the master only. A successor that takes over becomes primary, not a CA holder; if the master is gone for good, import the CA on another node by hand. Never on shared storage. - SignsNode certs (ECDSA P-256, 90 days), declarations, releases.
- ReleasesSignature verified before staging; unsigned refused. Members cannot fan out.
- Node channel9601, mTLS, explicit route allowlist, fingerprint checked every call.
- EvictionCerts revoked everywhere. Rotate cluster secrets after a hostile removal.
OS upgrades
os_auto_upgrades (/hosttune/settings): off by default; on = Debian unattended security upgrades daily. The panel itself: signed releases only.

