Skip to main content

Security Model

See what each part of a pfSense Actions deployment holds, how the controller protects it, and which gaps to cover with your network setup.

Why Use This Model?

  1. Keeps the firewall credential in one place, so each client holds only its own token.
  2. Checks every token and scope before any SSH work starts.
  3. Returns small JSON results, so firewall output doesn't leave through the API.
  4. Lists the current limitations, so you can plan network controls around them.

Trust Boundaries

PartHoldsDoesn't hold
Automation clientThe API address and its own personal access tokenpfSense credentials or other clients' tokens
Reverse proxyThe TLS certificate and the requests it forwardspfSense credentials
ControllerThe controller database and the encryption key that decrypts its secretsRaw tokens, raw recovery codes, passkey private keys, or a command endpoint
Data volume backupsEverything the controller stores, including the generated encryption keyRaw tokens or raw recovery codes
pfSenseThe automation account and its authorized SSH keyTokens or console accounts
Notification servicesThe alerts sent to them, which can include the firewall's address and SSH port, a locked-out client address, or a MAC addresspfSense credentials, tokens, or console accounts

The controller is one security boundary, and it doesn't replace network segmentation or pfSense's own access controls.

Tokens and Scopes

  • Each token carries 256 bits of random data, and only its SHA-256 token hash is stored.
  • Hashes are compared in constant time, and tokens are read from the database on every request, so a revocation applies at once.
  • Scopes are exact, and a request that fails the token or scope check never reaches SSH.
  • A token can't sign in to the console or change tokens, notifications, the connection, or sign-in settings, because those routes need a browser session and answer a token with 403.

Console Protections

  • Separate account — The administrator account has its own Argon2id-hashed password, and tokens or pfSense credentials can't sign in. Unknown usernames take as long to reject as wrong passwords.
  • First-run setup — Creating the account needs the setup code from the container log. Each code lasts 30 minutes and works once, and the setup screen allows 10 wrong codes per address in 15 minutes.
  • Session cookie — The session cookie is HttpOnly, SameSite=Strict, and Secure over HTTPS. A browser session lasts 24 hours from sign-in, and the database stores only a hash of its value.
  • Change protection — Every change needs the session's CSRF token in the X-CSRF-Token header. Changing the password ends every other session.
  • Password checks — Setting up or turning off the authenticator app, making new recovery codes, adding or removing a passkey, and changing the username or password all ask for the password again.
  • Sign-in limits — Wrong passwords, wrong codes, rejected passkeys, and wrong password confirmations share a limit of 10 failures per address in 15 minutes. Every route under /api/v1 also allows 120 requests per minute per address.
  • Second factors — TOTP codes can't be reused, and each recovery code works once and is stored as a hash. Passkey sign-in requires user verification and checks the origin, the hostname the passkey belongs to, and the signature counter.
  • Challenges — A passkey challenge works once, whether or not it succeeds. A sign-in challenge waiting for a code lasts 5 minutes and ends after 5 wrong codes.
  • Page headers — Every response sends Cache-Control: no-store, X-Content-Type-Options: nosniff, and Referrer-Policy: no-referrer, except the console's versioned files, which browsers may cache. Console pages add a strict Content Security Policy and X-Frame-Options: DENY.

Data at Rest

  • The controller database is created readable only by the service's user, inside a folder only that user can open.
  • The pfSense credential, the TOTP secret, and each notification destination's secret fields are encrypted with the encryption key, which is never written to the database. The generated key is always kept in keys/installation.key in the data directory, readable only by the service's user.
  • When the key file is missing, pfSense Actions makes a new key, removes the pfSense credential and the secret fields the old key sealed, and turns off the authenticator app, as described in Backup and Recovery. A key file from another database stops startup instead.
  • The administrator password is stored as an Argon2id hash, each token, session, and recovery code as a SHA-256 hash, and each passkey as its public key only.

info

Encryption uses AES-256-GCM with a random 12-byte IV for each value. Password hashing uses Argon2id with 64 MiB of memory, 3 passes, and 2 lanes, a 16-byte salt, and a 32-byte hash.

SSH Credential and Host Keys

  • A key is recommended over a password, and a key with a passphrase isn't supported.
  • Each request opens its own SSH session and closes it when the request ends, and so does each scheduled check.
  • The commands come from the allowlist and take only validated values, and no route accepts a command string.
  • pfSense Actions doesn't perform host key verification, so it's recommended to keep the network path to the firewall on a segment you trust.

Current Limitations

  • HTTPS is external — The listener serves plain HTTP, so HTTPS has to come from a reverse proxy or another encrypted path.
  • No host key verification — pfSense Actions accepts whichever host key the SSH server presents.
  • No command time limit — A command that hangs keeps its request open, and only the SSH connection attempt times out, after 20 seconds.
  • No graceful shutdown — Stopping the service ends requests and SSH sessions that are still running.
  • Forwarded headers from private addresses — pfSense Actions trusts X-Forwarded-* from any private or loopback address, so a device on your network that can reach the listener directly can choose the address its rate limits apply to.
  • One firewall and one administrator — Each deployment stores a single connection and a single account.
  • IP address only — The firewall is configured by IP address, not hostname.
  • Minimal logs — Logs are plain text without request IDs, and they don't explain a 502.
  • Local activity log — It keeps 250 events in the database and isn't tamper-resistant.
  • Alerts aren't retried — A notification that can't reach its service in 10 seconds, or that the service refuses, isn't sent again, as described in Notifications.
  • Listens on every interface — The Compose file publishes the port on every IPv4 and IPv6 address, and npm run dev and npm run prod listen on every interface, so devices on your network can reach the plain-HTTP listener unless you switch to a loopback binding.