Security
Arka's defences sit in layers: some act before a connection is even read, some on every request of a site, and some protect the server itself. Most are one short section in a host file.
The layers#
| Where | What Arka does | Configured with |
|---|---|---|
| At the door, before TLS | Banned addresses are disconnected as soon as they connect. Per-address and per-worker connection caps — HTTP/3 connections count too. TLS handshakes must finish within 10 s. Under a flood of HTTP/3 handshakes, new clients must first prove their address. When connections pile up, idle ones of the heaviest clients are closed first, so ordinary visitors keep getting in. | [bans], max_connections_per_ip, http3_validate_after, shed_idle_connections |
| The protocol | A strict HTTP parser that refuses request-smuggling tricks (both Content-Length and Transfer-Encoding, malformed chunks). Headers must arrive within 30 s (slowloris), and may not exceed 64 KB — over HTTP/3 too, where an oversized header is refused before it is read into memory. HTTP/2 "rapid reset" floods are cut off. Clients that stop reading (60 s) or stall while sending a body (60 s) are disconnected. |
[server] timeouts and limits |
| Each site | Firewall, rate limits, concurrent-request limits, IP and country rules, sign-in, signed links, body size limits, security headers, a bot check. | the host file |
| The server itself | Runs as an unprivileged user in a systemd sandbox; sites cannot reach Arka's own files, the system's folders or sockets that control the machine; the panel cannot change what would reach past it; updates must carry Arka's signature; written in a memory-safe language. | the package, arka.toml |
The web application firewall#
The firewall reads every request — path, parameters, form and JSON fields, uploads, cookies, headers — and refuses those that look like attacks: SQL injection, cross-site scripting, path traversal, remote code execution, probes for secret files, and known attack tools. It is off until you add a [waf] section to a host (or switch it on in the host's page in the panel).
Choose a protection level#
How much protection a site gets is one word:
[waf]
level = "standard" # basic, standard, strict or maximum
learn_until = "2026-11-01T09:00:00Z" # only log until then (UTC), then block by itself
| Level | Rules | Sensitivity | Checks | Bodies too large to check | Fits |
|---|---|---|---|---|---|
basic |
Arka's own | 1 | every request | start checked | Any site. About 1 µs a request; rarely refuses an ordinary visitor. |
standard |
OWASP Core Rule Set 4 | 1 | requests that can reach your application | refused (413) | Recommended. What the panel turns on. Static files and cached pages — where an attack has nothing to run — cost nothing extra; requests that reach the application pay about 0.1–0.2 ms. |
strict |
OWASP Core Rule Set 4 | 2 | every request | refused (413) | Sites that hold sensitive data. Catches more; most applications need a few exceptions. |
maximum |
OWASP Core Rule Set 4 | 3 | every request | refused (413) | The widest coverage, for sites ready to tune away false positives. |
Learn first. A firewall turned on from the panel only logs what it would refuse for its first 24 hours, then starts blocking by itself — no reload, nothing to remember. During that day the Firewall page lists everything it would have refused, and each entry has an Allow here button (see False positives), so a rule that does not suit your application is spared before any visitor is turned away. In a host file, learn_until does the same; Block now in the host's Web firewall section ends it early, and mode = "detect" logs for as long as you like.
Ordinary clients get through. At standard and strict, Arka starts the Core Rule Set from settings that let real traffic pass where the stock ones refuse it: uploads and webhooks that send Expect: 100-continue (curl, PHP's curl — shops' and payment providers' webhooks — and the AWS SDKs), analytics beacons (text/plain), upload APIs (application/octet-stream), REST methods (PUT, PATCH, DELETE) and WebDAV's, and GraphQL queries. A test runs requests real clients make through every level on every build — browsers over HTTP/1.1 and HTTP/2, forms, JSON and GraphQL APIs, beacons, uploads, webhooks, an S3 client, crawlers, a mobile app — and standard must let them all through. maximum is the rule set exactly as published.
A [waf] without a level is basic — configurations written before levels existed keep working exactly as before. Each thing a level decides can still be set on its own, and wins: level = "strict" with inspect = "apps" is the strict rules on application requests only; paranoia = 4 is the strictest level of all (it also refuses unusual methods, WebDAV's included). In the panel, the host's Web firewall section shows the levels as cards, and Custom opens every setting.
The two rule sets. Arka's own (basic) are about 50 rules modelled on the OWASP Core Rule Set, tuned to stay quiet on ordinary traffic and very cheap — ordinary requests run no regular expressions at all. From standard up, the firewall runs the complete OWASP Core Rule Set 4.30, the industry-standard rules of ModSecurity and Coraza, on Arka's own engine — which passes all 5,194 of the CRS project's own regression tests that concern the rules. It is compiled once per server (about 40 MB of memory, however many sites use it), and its engine is fuzzed with whole requests, bodies and answers included.
How it decides. Each rule that matches adds points to the request — 5 for a near-certain attack, 2 to 4 for weaker signs — and the request is refused when the total reaches threshold (5). Values are decoded the way the application would see them first (percent escapes several layers deep, HTML entities, Unicode tricks, SQL comments), so disguises do not get through. To try a stricter level before using it, log_paranoia = 2 runs its extra rules too but only logs what they would have refused.
Request bodies#
Bodies are checked whole before they reach your application, the way ModSecurity buffers them: bodies larger than 64 KB wait on disk (in Arka's spool, within buffer_disk_max) until the firewall has decided.
- Uploads (
multipart/form-data) are checked whole however large — every field and every file name, not the bytes of the files — so a field placed after a 100 MB file is still seen. - Forms, JSON, XML and text are checked whole up to
max_body(1 MB). - Larger ones are refused with
413fromstandardup, so nothing reaches the site unchecked; atbasiconly their start is checked. Change it withoversize = "refuse"or"inspect_start", or raisemax_bodyfor a site that posts very large forms.
A refusal shows on the Firewall page as protocol.body-too-large.
False positives#
…are fixed precisely with an exception — some rules, on some paths, for some fields only:
[[waf.exceptions]]
rules = ["xss"] # a rule id (942100 for the Core Rule Set), a group, or "*"
paths = ["/admin/posts/"]
args = ["content", "body*"] # only these fields
The Firewall page in the panel shows every refused (or, while learning, flagged) request, which rules matched and their ids. Allow here on an entry writes that exception for you — the rules it matched, on its path and below — into the host file, checked and saved like any change and recorded in the activity log. Your own rules go into [[waf.custom]]. The reference has every rule and option.
Automatic bans#
Addresses that keep misbehaving are refused at the door for a while — on every site and every stream, before the TLS handshake. Turn it on in /etc/arka/arka.toml:
[bans]
enabled = true
threshold = 20 # points…
window = "10m" # …within ten minutes earn a ban
duration = "15m" # the first ban; each later one lasts twice as long
max_duration = "1d"
never = ["127.0.0.0/8", "::1", "203.0.113.4"] # your monitoring and office
[bans.points]
waf = 5 # a request the firewall refused
auth = 2 # a failed sign-in
rate_limit = 1 # a request over a rate limit
not_found = 0 # 404s — raise it on sites that are probed a lot
Four refused attacks within ten minutes earn a 15-minute ban; a second ban lasts 30 minutes, then an hour, up to a day. Only attacks count: a request refused for its protocol alone — a missing header, an unusual method or content type — is refused, but its sender is not banned for it, so one odd client cannot get a real visitor banned from every site. A sudden wave of refusals on a site, or of automatic bans, raises an alert. IPv6 clients are banned by their /64 network. Bans survive restarts. The Firewall page shows current bans, who is getting close to one, and lets you ban or release any address or network by hand (that works even with automatic bans off).
Before you turn bans on behind a CDN or load balancer, make sure Arka sees real visitor addresses (
real_ip) — otherwise it would ban the proxy. A trusted proxy itself is never banned for what it forwards.
Rate limits and connection limits#
[rate_limit] # per visitor
requests_per_second = 20
burst = 40 # short bursts allowed
[connections]
per_client = 10 # requests in flight at once per visitor
Over the limit, visitors get 429 Too Many Requests with Retry-After. Both work per route too, so a login page can be much stricter than the rest:
[[routes]]
path = "/login"
rate_limit = { requests_per_second = 1, burst = 5 }
handler = { type = "proxy", upstreams = ["127.0.0.1:8000"] }
A limit can count something other than the address, for example an API key: key = "{header.X-API-Key}".
IP and country rules#
[access]
allow = ["10.0.0.0/8", "203.0.113.4"] # when set, everyone else gets 403
deny = ["198.51.100.0/24"] # checked first
[geo] # needs a country database, see below
deny = ["XX"] # ISO country codes
Country rules need a free country database (MaxMind GeoLite2 Country or DB-IP Lite, .mmdb), named in arka.toml with [geoip] database = "/etc/arka/GeoLite2-Country.mmdb". Both sections also work on single routes, for example to keep /admin/ to the office network.
Bot check#
A [challenge] section shows visitors that look like bots — no browser signature, scripting tools, AI crawlers — a page that solves a small proof-of-work puzzle in the background (a fraction of a second on a phone), then lets them in for a day. Clients that do not run JavaScript, which is most scrapers, never get past it. Real search engines are recognised by DNS and let through. See [challenge].
Sign-in for your sites#
Put a sign-in in front of a whole site, or one route, without changing the application:
| Type | For | Example |
|---|---|---|
basic |
a few people, a staging site | auth = { type = "basic", users = ["alice:$2b$…"] } — hashes from arka passwd alice |
oidc |
company accounts: Google, Microsoft Entra ID, Okta, Keycloak, GitLab… | recipe |
ldap |
OpenLDAP, Active Directory, FreeIPA | url = "ldaps://ldap.example.com", bind_dn = "uid={user},ou=people,dc=example,dc=com" |
forward |
an existing auth service: Authelia, Authentik, oauth2-proxy | url = "http://127.0.0.1:9091/api/verify" |
jwt |
APIs whose clients carry a token | secret = "…" or public_key = "keys/jwt.pem" |
A route can opt out with auth = { type = "none" } — for example a health check or webhook. Failed sign-ins count towards bans. All options are in the reference.
HTTPS settings#
Certificates come from ACME automatically (acme = true), and the defaults are already modern: TLS 1.2 and 1.3 only, strong ciphers, OCSP stapling, session resumption. Worth turning on for every site:
[tls]
acme = true
redirect_http = true # send http:// to https://
hsts = true # browsers refuse plain HTTP for a year
# min_version = "1.3" # if all your visitors have recent browsers
Mutual TLS — only clients with a certificate from your own CA get in — is client_auth = { ca = "/etc/arka/clients-ca.pem" }. Builds with the post-quantum option (pq) offer the hybrid key exchange X25519MLKEM768 that current browsers support.
Security headers#
[headers]
security = true
adds X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, Cross-Origin-Opener-Policy: same-origin and a Permissions-Policy that turns off the camera, microphone, geolocation, payment and USB, to every response that does not set them itself. A Content Security Policy is the strongest defence against cross-site scripting but specific to your pages: roll one out with csp_report_only = true first, then enforce it.
Signed links and upload scanning#
Signed links let only links you hand out — valid until a date — reach a route, for paid downloads or private files: signed = { secret = "…" } on the route, and arka sign-url <host> /downloads/report.pdf --expires 7d (or Make a link in the panel) to create one.
Virus scanning with ClamAV checks files uploaded to WebDAV shares and through the panel — and, with scan_apps = true, uploads to your applications (WordPress media, forms) — before they are stored. Infected uploads are refused and count towards a ban. See [antivirus].
Protecting the control panel#
The panel can change every site, so it deserves the most care:
- Keep it off the internet. Leave port 9898 closed and use an SSH tunnel, or list only your addresses in
allowunder[admin]. Out of the box it answers only this machine and private networks. - Use personal accounts with two-step sign-in, and keep the token for emergencies. With
token_local_only = true— set in the package'sarka.toml— the token then only works from the server itself (an SSH tunnel counts). - Give people the least role they need — most colleagues need viewer or editor, not admin.
- Give the panel a real name and certificate (how), so browsers trust it and passkeys work.
- Leave the file manager off unless you need it: it can read any file the server can, keys included. Only
arka.tomlon the server can switch it on — not the panel. - If the token may have leaked, replace it:
sudo -u arka arka --config /etc/arka/arka.toml token --rotate. The running server switches to the new token at once; signed-in accounts stay signed in.
What the panel cannot change#
Some settings would let whoever sets them reach past the panel — run programs, read every file, skip two-step sign-in. The panel shows them but cannot change them, so a stolen panel session stays a panel session; they are changed in /etc/arka/arka.toml on the server:
| Setting | Why |
|---|---|
admin.file_manager |
It gives the panel every file on the machine (the panel may switch it off). |
admin.token_local_only |
The token skips two-step sign-in (the panel may switch it on). |
admin.token |
Never shown in the panel, and kept as it is when the panel saves. |
acme.dns_hook |
It names a program Arka runs. |
update.url, update.public_key |
Where new versions come from, and who may sign them. |
server.web_roots, server.allow_sockets |
Which folders sites may publish, and which machine-controlling sockets they may reach (below). |
What sites cannot reach#
Whoever writes a host — an editor in the panel, or a mistake — it stays inside the sites:
- Arka's own files: a host may not serve, log to or include
arka.toml, the token, the configuration folders or/var/lib/arka— symbolic links included. - The system's folders are never served:
/etc,/proc(every process's environment),/root,/run,/sys,/usr,/var/lib,/var/logand the like — nor a folder that holds them, such as/or/var. - A log file is never named like a script (
.php,.cgi…): a request logged into a PHP site's folder would otherwise run as code. - Sockets that control the machine are never proxied to: Docker's, containerd's, Podman's, systemd's, D-Bus, libvirt's, snapd's, LXD's. One site proxying to the Docker socket would hand root to anyone on the internet. A site that really must is listed in
allow_sockets. - With
web_roots—["/var/www", "/srv"]on a new install — everything a site serves lies inside those folders:
# /etc/arka/arka.toml
[server]
web_roots = ["/var/www", "/srv", "/opt/shop/public"] # not /home: the service cannot see it
# allow_sockets = ["/var/run/docker.sock"] # only if a site truly needs it
Files are served only from inside a site's root; symbolic links that lead out are refused unless you allow them.
How Arka protects itself#
- No root. The service runs as the
arkauser. The only privilege it has is opening ports below 1024. - A sandbox. systemd makes the whole file system read-only for Arka except its own three folders, hides home directories, and adds: no privilege escalation, no kernel tunables or modules, a system-call filter, no writable-and-executable memory, and a watchdog that restarts a stuck server.
- Its configuration is private.
/etc/arkais readable by root and Arka only — host files can hold passwords and keys, and a PHP site'swww-datamust not read them. Files saved from the panel keep the permissions they had (new ones are0640), and are written crash-safely: a power cut leaves the old file or the new one, never half of one. - Signed updates. Every release is signed with an Ed25519 key kept offline. Arka checks the signature against the key built into it, and the root helper checks it again before installing — so even someone who took over the Arka process could not install their own code as root. Older versions are never installed, so a validly signed old release cannot be replayed.
- Memory safety. Arka is written in Rust, so the buffer overflows and use-after-free bugs behind many web-server vulnerabilities are ruled out in Arka's own code. Every parser that reads untrusted bytes — requests, the PROXY protocol, TLS ClientHellos, HTTP/3 frames, upstream and FastCGI responses, JWTs, WebDAV XML, MP4 files, DNS answers, and whole requests through both firewall rule sets — is fuzzed and property-tested.
A hardening checklist#
- Ports 80 and 443 open; 9898 closed or limited with
allow. [acme] emailset, every site on HTTPS withredirect_httpandhsts.[headers] security = trueon every site (a snippet makes it one line).[waf] level = "standard"(or higher) on every site — turned on from the panel, it learns for a day before blocking.[bans] enabled = true, with your own addresses undernever.- Stricter
rate_limiton login and API routes. [real_ip]set if you are behind a CDN or load balancer; with the PROXY protocol,proxy_protocol_fromlists the load balancers (Arka warns when it is empty).web_rootslisting the folders your sites live in.[alerts]sending to a webhook you watch.- Applications listen on
127.0.0.1only. - Personal panel accounts with two-step sign-in; the token stored safely.
- Backups of
/etc/arkaand/var/lib/arkacopied off the server (how). - Updates installed when the panel offers them.
Reporting a vulnerability#
Found a security problem in Arka? Please tell us privately first — see Security and disclosure.