Security
This page covers what an administrator has to set up and look after to run Nexmap safely. Every setting mentioned is described in the configuration reference.
TLS and the reverse proxy
nexmapd speaks plain HTTP. Run it behind a reverse proxy that terminates TLS (on Coolify, the built-in
Traefik does this), and do not expose its port to the network directly:
-
Leave
NEXMAP_COOKIE_SECUREattrue. Browsers then send the session cookie over HTTPS only, and the server sendsStrict-Transport-Security, so browsers that have visited once refuse plain HTTP for a year. -
The client address of sessions, failed logins and the limits below comes from the
X-Forwarded-Forheader, but only when the request arrives from a trusted proxy (NEXMAP_TRUSTED_PROXIES, by default loopback and private networks). A request from any other address is taken to come from that address, whatever its header says. From a trusted proxy the header is read from the end, skipping trusted proxies, so entries a client wrote itself do not count, whether the proxy replaces the header or appends to it. -
If machines other than the proxy can reach
nexmapdon a private network, setNEXMAP_TRUSTED_PROXIESto the proxy’s address. Otherwise they can claim any client address and evade the limits. -
The server refuses form posts and API calls that a browser sends on behalf of another site. Modern browsers state the origin of a request themselves; for older ones the server compares the
Originheader with theHostheader, so the proxy has to pass the originalHoston (Traefik and Caddy do; nginx needsproxy_set_header Host $host). SettingNEXMAP_PUBLIC_URLmakes that comparison independent of the proxy.
Response headers
Every response carries a Content-Security-Policy that only allows scripts, styles, images, fonts and
form targets from the server itself and forbids framing, plus X-Content-Type-Options,
X-Frame-Options, Referrer-Policy and Permissions-Policy. Images in Markdown that point to another
server are therefore not loaded. The policy still allows inline scripts, which the pages use, so it
limits what injected markup could do (load code from or send data to another server) rather than ruling
it out. If your proxy adds headers of its own,
make sure it does not replace these with weaker ones.
Request limits
Request bodies are capped at 1 MiB, attachment uploads at NEXMAP_MAX_UPLOAD_BYTES plus 1 MiB for the
form around the file, and test run uploads at 32 MiB; larger requests get 413. A request has to be
received within 5 minutes and answered within 10.
The encryption key
Personal data (email addresses and display names) is encrypted in the database with AES-256-GCM.
Usernames are treated as pseudonyms and are not encrypted. The key comes from NEXMAP_ENCRYPTION_KEY,
a JSON list of keys with an ID and a base64-encoded 32-byte key each:
[{"id": "2026-01", "key": "<32 random bytes, base64-encoded>"}]
To generate one:
printf '[{"id":"%s","key":"%s"}]\n' "$(date +%Y-%m)" "$(openssl rand -base64 32)"
Treat the key like a database password: keep it in your platform’s secret store, not in a repository.
Each encrypted value, including a user’s two-factor secret, is bound to the record and field it belongs to. Someone who can write to the database but does not have the key cannot move one user’s value into another user’s record: it no longer decrypts there. Values written by a version before this binding existed are still read and are bound the next time the record is saved.
Back up the key separately from the database. Without it, the personal data in a database backup cannot be decrypted, and Nexmap has no way to recover it. Conversely, a database backup stored together with its key protects nothing. See Backup and restore.
Rotating the key
The first key in the list encrypts new data; the others are kept only to decrypt data written with them. To switch to a new key, put it first and keep every older key in the list:
[{"id": "2027-01", "key": "<new key>"}, {"id": "2026-01", "key": "<old key>"}]
Never remove an old key: data encrypted with it would become unreadable.
Rotation is incomplete today, so rotate only when you have a reason to, such as a leaked key:
- Existing data is not re-encrypted. It stays encrypted with the old key until the record is next changed, so a leaked old key still exposes it.
- Email addresses also get a lookup hash derived from the first key, which is how the database prevents two accounts from sharing an address. After a rotation, that check only compares addresses hashed with the same key, so a duplicate of an address stored under the old key is not caught.
The first administrator
On the very first start, when the database has no users, nexmapd creates an administrator from
NEXMAP_ADMIN_USER and NEXMAP_ADMIN_PASSWORD. If the username is unset it is admin; if the password is
unset, a random one is generated and written to the log.
Set both variables for the first start. If you rely on the generated password, read it from the log, sign
in, change the password right away, and keep in mind that the log still contains it. The administrator’s
email address starts out as admin@localhost; change it on the profile page, or a password reset link
could not reach you.
Once users exist, both variables are ignored and can be removed.
Accounts and sign-in
-
Two-factor authentication: users can set up TOTP on their profile page. It can be required in two places:
- Instance-wide, under Two-Factor Authentication in the instance settings (
/admin/settings): for all users, for administrators, and for product owners, developers, testers or viewers. A role switch covers users who hold that role in at least one product. Users covered are sent to the setup page after signing in and cannot use Nexmap until 2FA is set up; API calls with their session get403 TOTP_SETUP_REQUIRED. A common setup is to require it for administrators and product owners and leave the rest to the products. - Per product, by its owners under Settings → General (Require two-factor authentication).
Members without 2FA, administrators included, cannot open that product until they set it up; their
other products keep working. The dashboard marks such products with a lock, and API calls to them
get
403 TOTP_SETUP_REQUIRED. Only a user with 2FA can turn the requirement on, and the team page shows owners which members still lack it.
Personal access tokens are not affected by either: a token is a separate credential. Users cannot turn 2FA off while any rule requires it of them. An administrator can reset a user’s 2FA from the user’s detail page, after which that user can sign in with the password alone until they set it up again (and is sent to setup, or kept out of 2FA products, as above). Should the server be unable to read the 2FA settings or a user’s product roles for a while (a database fault), it keeps acting on the values it read last, so enforcement does not lapse in the meantime.
- Instance-wide, under Two-Factor Authentication in the instance settings (
-
Usernames and email addresses are unique regardless of case:
Admincannot be registered next toadmin, and signing in accepts either spelling. -
Passwords are stored as salted PBKDF2-SHA256 hashes (
NEXMAP_PBKDF2_ITERATIONS). The instance settings can require uppercase and lowercase letters, digits and special characters in new passwords, and a minimum length from 8 to 128 characters. -
The password is asked for again before two things a session alone must not be enough for: deleting your own account, and creating an access token with the
accountoradminscope. -
The last administrator cannot be locked or deleted, by anyone including themselves, and a product always keeps at least one owner. There is no setting to make a user an administrator yet; the first administrator is the only one unless you change
is_adminin the database. -
Forgotten passwords: with mail, users can ask for a reset link on the sign-in page (see Email). Without mail, administrators set new passwords for users from the user’s detail page. Either way, the new password ends all of that user’s sessions, and a second factor stays on.
-
Sessions expire after
NEXMAP_SESSION_TTL(24 hours by default). The database stores only a hash of each session token, so a database backup contains nothing that works as a session cookie. Changing a password ends the user’s other sessions, keeping only the one the change was made from; an administrator setting a new password ends all of that user’s sessions. Personal access tokens stay valid either way; revoke them separately if they may have leaked. Locking an account blocks its sessions and tokens at once. -
Failed logins are limited per client address: after 5 wrong passwords for the same username, or 20 for any usernames, within 15 minutes, that address has to wait until the 15 minutes are over. The web form says so and the API answers
429 TOO_MANY_ATTEMPTSwith aRetry-Afterheader. A correct password clears the count for that username. There is no limit per account across addresses, so nobody can lock another user out by guessing their password. The counters live in memory and reset when the server restarts. -
Wrong two-factor codes are limited per user: after 10 wrong codes (from the authenticator or recovery codes) within an hour, no code is accepted for that user until the hour is over, at sign-in and when changing the two-factor settings alike. This takes the user’s password or session, so it cannot be used to lock out a stranger; a user who is blocked this way should change their password. The server logs
second factor blocked after repeated wrong codeswith the user ID.
Records of administrative actions
Changes to a product’s team (adding and removing members, changing roles, transferring ownership) are
recorded in the product’s audit trail. Administrator actions (approving, locking, unlocking and deleting
users, setting passwords, resetting 2FA, changing instance settings) are written to the server log as
administrator action with the administrator’s and the affected user’s ID and the client address. Keep
the log if you need to trace them later.
New accounts
People get an account in one of two ways:
- Invitation: a product owner invites someone to their product with a role (on the product’s Team
page), or an administrator invites someone to the installation (
/admin/invitations). With mail, the inviter chooses whether Nexmap emails the link to the invited address or shows it to the inviter to pass on; without mail, Nexmap always shows it (built fromNEXMAP_PUBLIC_URLif set). Whoever holds the link can create the account, so treat it as a secret: it works once, expires after 7 days, and is never written to the log. An emailed link is not shown to the inviter, so the new account has proven that its owner reads that mailbox. Accounts created from an invitation skip moderation. - Self-registration, if self-registration is enabled in the instance settings: anyone who can reach
the server can create an account on
/register. With moderation enabled, such accounts wait for an administrator’s approval under/admin/usersbefore they can sign in. A self-registered account belongs to no product until a product owner adds it.
Creating accounts is limited to 20 attempts per client address and hour, for self-registration and
invitations together; after that the form and the API (429 TOO_MANY_ATTEMPTS) ask to wait.
Only product owners (and administrators) can invite to a product, and only administrators can invite to the installation without a product.
Nexmap sends email only when configured, and then only invitations and password reset links. It sends nothing else (no notifications), and no part of Nexmap lets anyone choose what an email says. The safeguards:
- Fixed text. Both emails are fixed text with the address of the installation, the link and its expiry; an invitation also names the role. Nothing a user typed (product names, display names) is put into an email, so nobody can use Nexmap to carry their own message to an address of their choosing.
- One recipient. Every email goes to exactly one plain address. Lists, display names and line breaks are refused, so the recipient and headers cannot be manipulated.
- Invitation emails need a signed-in product owner or administrator (or their access token with
settings:writeoradmin). Each inviter can send 20 an hour, and each address receives at most 3 a day, from all inviters together. Beyond that, the form and the API (429 TOO_MANY_ATTEMPTS) ask to wait; invitation links can still be created. - Password reset links go only to the address stored on an existing, active account, never to an
address typed into the form. The form at
/forgot-passwordanswers the same for every input, and sends in the background, so neither the answer nor its timing tells whether an account exists. A client address may ask 5 times an hour, and an account receives at most 3 reset emails an hour, whoever asks. There is no API endpoint for it. - Reset links work once and for 60 minutes; asking again replaces the previous link. The database stores only a hash of the token. Using the link sets the new password, ends all of the user’s sessions, and leaves two-factor authentication on: the user signs in anew with the second factor. Locked accounts, and accounts awaiting approval or deletion, cannot use a reset link.
- A cap for the installation,
NEXMAP_MAIL_MAX_PER_HOUR(100 by default), limits all emails together, whatever the other limits let through. - Links point to
NEXMAP_PUBLIC_URLonly, which is required with mail. A reset link built from the request’sHostheader could be made to point to someone else’s server. - Tracking is off for the mailer APIs that have a switch for it (Mailgun, SendGrid, Lettermint): tracking would route every link, token included, through the provider’s redirector.
- SMTP is encrypted: with
starttls, Nexmap refuses to send through a relay that does not offer STARTTLS, and it sends credentials over TLS only.
The limits are kept in memory and reset when the server restarts, like the login limits. Reset links waiting to be sent are kept in memory too; one still queued when the server stops is lost, and the user asks again.
Access tokens
Personal access tokens are what the CLI, CI jobs and AI agents use. They are accepted only by the REST API, never by the web UI’s pages. Users can narrow a token to some products, to read or write access per area, and to a lower role (see Restricted tokens); a token never grants more than its user holds at the time of the request.
Two rights are never granted by default: managing the user’s own account and tokens (account), and
administration (admin). Granting either from the web UI or a session needs the user’s password. An administrator’s token without the admin scope has no administrator rights,
so a leaked CI token cannot reach the instance settings. A token can create further tokens only with the
account scope, and only narrower than itself.
Under Instance Settings, Maximum Token Lifetime forces every new token to expire within the given
number of days (0, the default, allows tokens without expiry). The token list shows when each token was
last used; revoke those that are no longer needed under API Tokens (/user/tokens).
Attachments
By default, every file type can be uploaded. To restrict this, set NEXMAP_ALLOWED_UPLOAD_TYPES; the
server then also checks that the content matches the extension. Attachments are always served as
downloads and never rendered by the browser, but Nexmap does not scan them for malware.
Development mode
NEXMAP_DEV_MODE fills a new database with demo users whose password is publicly known (it is in the
source code and the log). Never enable it on a server that others can reach.