Security
What monoki defends against, how, and — just as importantly — what it does not.
Authentication
Session cookies only. No API keys, no OAuth, no tokens to leak in a shell history.
- Cookie
monoki_session:HttpOnly,SameSite=Lax, andSecurewhenever the request arrived over TLS (directly or viaX-Forwarded-Proto). - The cookie value is 256 bits of randomness. Only its SHA-256 is stored, so a database copy does not hand over live sessions.
- Sliding 30-day expiry.
- Passwords: argon2id, 64 MiB / t=1 / p=4, parameters stored in the hash so they can be raised later without a migration. Verification is constant-time.
- Login is rate-limited per source address: 10 failures in a minute locks that address out for 5 minutes, and the lockout holds even for a correct password.
- A wrong password and a disabled account return the same generic error, so login cannot be used to enumerate accounts.
Authorization
Every access decision lives in one file (internal/api/authz.go); the storage
layer enforces nothing and takes no "acting user" argument, so there is exactly
one place to audit.
- A bookmark has no permissions of its own. It inherits its group's, entirely.
- Asking for something you have no access to returns 404, not 403 — a stranger must not be able to tell an id that exists from one that does not. A 403 is only for "you can see this, but not do that".
- Icons are authorized exactly like their bookmark. There is no unauthenticated icon URL, however well it would cache.
- Admin role grants user management and the audit log. It grants no access to anyone's groups. See Users & roles.
Server-side fetching (SSRF)
monoki fetches URLs a user typed, from inside your network. This is its one genuinely risky surface, and it is treated that way.
- The address check runs at dial time, not before it. A pre-flight DNS
lookup would be checking a different resolution than the connection actually
uses, so a hostname answering
1.2.3.4once and127.0.0.1a moment later would walk straight past it. Checking the concrete IP as the socket is opened closes DNS rebinding, redirect-to-internal, and multi-record hosts in one place — and it re-runs for every hop of a redirect chain. - IPv4-mapped IPv6 addresses are unmapped before classification, so
::ffff:127.0.0.1is recognised as loopback. - Redirects are capped at 5 hops and re-validated per hop.
- Layered timeouts: 5s dial, 5s TLS handshake, 5s response headers, 10s total, 15s for the whole request.
- HTML is read up to 512 KiB, icons up to 256 KiB. Oversize icons are rejected, not truncated.
- Content types are allowlisted and cross-checked against the actual bytes.
- URLs with embedded credentials are refused, as are non-HTTP schemes.
- Lookups are rate-limited per user.
MONOKI_ALLOW_PRIVATE_FETCH defaults to true, which is the deliberate
exception: bookmarking http://nas.lan:5000 is the product's main use case,
and it requires reaching a private address. Set it to false if your instance
has users you would not trust with a curl on the server. All the other
controls above apply either way. See Configuration.
Stored icons
Icon bytes come from arbitrary sites, and monoki serves them from its own
origin — so every icon response carries X-Content-Type-Options: nosniff,
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox, and Content-Disposition: inline. SVG is accepted because too many
modern sites offer nothing else; the UI only ever renders icons through an
<img> tag, which does not execute SVG script regardless.
Audit log
Every mutating action writes a row with a dotted action name — group.share,
bookmark.delete, user.update. It is readable by admins.
The log deliberately has no foreign key to the accounts table, so deleting a user leaves their trail intact rather than cascading it away.
What monoki does not do
Stated plainly, because a security page that only lists strengths is not much use:
- No second factor. Passwords are the only credential.
- No encryption at rest. Anyone who can read
monoki.dbcan read every URL, every icon, and every password hash. Protect the file. - No CSRF tokens. The defence is
SameSite=Laxplus the fact that every mutating endpoint requires a non-GET method and a JSON body. That is adequate for current browsers and is not defence in depth. - No account lockout beyond per-address rate limiting. An attacker with many source addresses is limited only by password strength.
- No egress allowlist. With private fetching on, any account can make your server issue a GET to any address it can reach. The response is reduced to a title and an image, but the request itself happens.
- No protection against a hostile admin. An admin can reset any password and then sign in as that person. There is no key escrow and no separation of duties.
- Not audited. No third party has reviewed this.
Reporting a problem
Open a confidential issue on the GitLab project rather than a public one.