Join our Newsletter — 33% off our NHI Course
Home› Guides› Elixir and Phoenix Security: Secure Coding and BEAM…
Guide Application Security

Elixir and Phoenix Security: Secure Coding and BEAM Deployment Hardening

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 8 November 2026 14 min read
On this page

Phoenix ships with strong defaults, so the serious flaws in Elixir applications tend to come from the few places those defaults are bypassed: a GET route on a state-changing action, String.to_atom/1 on user input, a distribution cookie that lets anyone on the network run code, or a remote shell exposed to the wrong interface. Elixir runs on the BEAM, the Erlang virtual machine, which brings risks most web security checklists never mention: a finite atom table that crashes the whole VM when full, a clustering protocol in which one compromised node means every node is compromised, and serialised terms that can carry executable functions. This guide covers authentication and sessions in Phoenix, CSRF and WebSocket protections, Sobelow, atom exhaustion, the distribution cookie and EPMD, secrets such as secret_key_base, logging and a deployment baseline. It is for Elixir developers, platform engineers and application security reviewers, and draws on the Phoenix security guide, the Erlang Ecosystem Foundation (EEF) Security Working Group guidance and the Sobelow project as checked in November 2026. General application security practice is in the Application Security Guide; this page stays on the BEAM specifics.

This is an independent guide from NHI Mgmt Group, based on Phoenix's public documentation as of November 2026. It is not sponsored or reviewed by Phoenix. Product features change, so confirm settings against Phoenix's current documentation.

Key takeaways

  • Never create atoms from untrusted input. Atoms are never garbage collected and the table defaults to 1,048,576 entries; when it fills, the VM crashes. Use lookup tables or String.to_existing_atom/1.
  • The distribution cookie is a root credential. Any process that can reach the distribution port with the cookie can run arbitrary code on every node. Use TLS distribution, bind it to a private interface, or turn it off with RELEASE_DISTRIBUTION=none.
  • CSRF protection only covers what you route correctly. protect_from_forgery checks POST, PUT, PATCH and DELETE; adding a GET route to the same action reopens it. Sockets need check_origin left on.
  • Cookie sessions cannot be revoked server side. The default store signs but does not encrypt, and logging out does not invalidate a captured cookie. Use the phx.gen.auth token approach and set encryption_salt.
  • Run Sobelow and the Hex audits in CI. Sobelow finds insecure configuration, raw SQL, raw/1 use, atom creation and unsafe deserialisation; mix hex.audit and mix deps.audit cover dependencies.

How identity and access work in Phoenix applications

Phoenix has no built-in user system. Most applications use the mix phx.gen.auth generator, which writes authentication code into the project rather than adding a library, so the team owns and must review it. Requests pass through router pipelines: the default :browser pipeline fetches the session, applies protect_from_forgery and put_secure_browser_headers, while an :api pipeline typically skips sessions and CSRF and relies on tokens.

MechanismDefault behaviourWhat to change
Session store (Plug.Session, cookie store)Session state lives in a signed cookie; HttpOnly set by default; Secure set only if Plug sees the request as HTTPSSet encryption_salt so contents cannot be read; configure TLS offload so Secure is applied
Session revocationNot possible with the cookie store alone: logout clears the browser cookie only. The only global invalidation is rotating secret_key_base or the signing salt, which signs out everyoneUse server-side session records: phx.gen.auth stores a session_token in the user_tokens table and deletes it on logout and password change
Session lifetimephx.gen.auth invalidates sessions after 60 days by defaultShorten for sensitive applications and add an idle timeout checked server side
Session fixationphx.gen.auth calls renew_session/1 on sign in and outKeep the call in custom flows; use configure_session(conn, renew: true) on privilege changes
AuthorisationNone built inAuthorise from conn.assigns.current_user, never from IDs in request parameters
  • Mass assignment: Ecto changesets copy only the fields passed to cast/3. Casting :is_admin or :role in a public registration changeset is a direct privilege escalation. Keep separate changesets for user and admin edits.
  • WebSockets and LiveView: a socket is authenticated once when it connects, and event handlers do not normally check whether the session is still valid. phx.gen.auth subscribes each socket to a per-session channel and broadcasts disconnect on logout; custom authentication must do the same. See the WebSocket Security Guide.

Secure coding risks specific to Phoenix and the BEAM

The Phoenix security guide and the EEF guidance converge on a short list of functions and patterns to search for in review. Most are safe by default and only become dangerous when developers reach for an escape hatch.

RiskDangerous patternSafe alternative
CSRFA GET route pointing at an action that changes state (action re-use CSRF); protect_from_forgery removed from a pipelineState changes only on POST, PUT, PATCH or DELETE through the :browser pipeline. See the CSRF Prevention Guide
Cross-site WebSocket hijackingcheck_origin: false on a socket that authenticates by session cookieKeep check_origin (defaults to the endpoint host); if it must be off, authenticate with a token parameter, not cookies
Atom exhaustion (denial of service)String.to_atom/1, List.to_atom/1, Module.concat, interpolated atoms such as :"field_#{x}" on inputLookup table of allowed values, or String.to_existing_atom/1 and Module.safe_concat; JSON parsers configured to return strings
Unsafe deserialisation:erlang.binary_to_term/2 on untrusted data, even with [:safe], which does not stop executable function termsJSON or another standard format; if External Term Format is unavoidable, Plug.Crypto.non_executable_binary_to_term/2
Cross-site scriptingUser input in Phoenix.HTML.raw/1, HTML built in html/2 or send_resp/3, content type taken from inputTemplate interpolation, which escapes by default; restrict upload content types
SQL injectionInterpolation into Repo.query, Ecto.Adapters.SQL.query or streamEcto query syntax; fragment with ^ parameters (interpolation is rejected at compile time); $1 placeholders. See the SQL Injection Prevention Guide
Code and command executionInput reaching Code.eval_string/3, Code.eval_quoted/3, EEx eval_string, System.cmd/3, System.shell/2, :os.cmd/2Never evaluate input; pass fixed executables and argument lists
Server-side request forgeryURLs built from input; trusting Req base_url or Tesla BaseUrl as a boundary (absolute URLs override them)Allowlist destinations; block 169.254.169.254 and internal ranges. See the SSRF Prevention Guide
CORSCORSPlug with a broad regex such as ~r/^http.*/Explicit origin list

Monitor erlang:system_info(atom_count) against atom_limit, as the EEF recommends: a count that keeps rising after startup means something is creating atoms at runtime and will eventually take the node down.

Admin roles and privileged access

The most privileged access to a running Elixir system is not an admin page: it is the remote shell and the distribution protocol. bin/my_app remote attaches an IEx shell to the live node, and bin/my_app rpc "EXPR" evaluates code in it. Both work by joining the node over Erlang distribution with the release cookie, so whoever holds the cookie and can reach the port has full control of the node and, in a cluster, of every node.

  • Cookie handling: if no cookie is configured, mix release writes a random one to releases/COOKIE on first assembly, and at runtime the RELEASE_COOKIE environment variable takes precedence. The Elixir documentation recommends a long random value such as Base.encode32(:crypto.strong_rand_bytes(40)). Never commit it, never share one cookie across environments, and remember it is baked into every build artefact that contains releases/COOKIE.
  • Cookies are not enough on their own: the EEF notes cookie authentication uses challenge and response but has no protection against an active man-in-the-middle, and default distribution traffic is unencrypted. Use TLS distribution with {verify, verify_peer}, a cacertfile and, preferably, mutual TLS.
  • EPMD exposure: the Erlang Port Mapper Daemon listens on TCP 4369 and answers unauthenticated name lookups, revealing node names and distribution ports. Bind it with ERL_EPMD_ADDRESS and bind distribution with the inet_dist_use_interface kernel option to a private cluster network.
  • Single-node applications: set RELEASE_DISTRIBUTION=none (the default is sname) or bind to loopback only and reach the shell over an SSH tunnel or VPN, never directly.
  • No distribution between tiers: distribution removes all isolation between nodes, so the EEF advises using it only for horizontal clusters, with HTTP or database interfaces between layers.
  • Operational dashboards: Phoenix LiveDashboard exposes process, ETS and request detail. Its README says to put authentication in front of it in production, for example the admin check generated by phx.gen.auth.
  • Erlang SSH daemon: Inventory any application that starts :ssh daemons, apply Erlang/OTP security updates promptly and follow the Erlang/OTP security advisories.

Integrations, API tokens and service accounts

Secret or machine identityWhere it livesControl
secret_key_baseEndpoint configuration, read from SECRET_KEY_BASE in config/runtime.exs; generated with mix phx.gen.secretUnique per environment, in a secrets manager; rotating it invalidates all sessions and signed tokens
Session signing_salt and encryption_saltEndpoint session optionsSet both; salts are not secret on their own but changing them is a revocation lever
Release cookieRELEASE_COOKIE or releases/COOKIEInjected at runtime; file permissions restricted; rotated if a build artefact leaks
Database and API credentialsconfig/runtime.exs from environmentNever in config/prod.exs, which is compiled into the release
Signed tokens for APIs and email linksGenerated with Phoenix.Token or the user_tokens tableShort max_age, single use for reset and confirmation links
Hex packages and build toolingmix.lock, CI runnersDependency code runs on build servers as well as in production; review additions and updates

Prefer config/runtime.exs for every secret: values in compile-time config end up inside the release and in any image built from it. Treat outbound API keys used by the application as non-human identities with an owner and a rotation date. See the Secrets Management Guide. For dependencies, the EEF recommends mix hex.outdated for available updates, mix hex.audit for retired package versions and mix deps.audit (the mix_audit project) against the Elixir Security Advisories database, plus an SBOM for wider tooling. See the SBOM Guide.

Logging and detection

  • Parameter filtering: Phoenix logs request parameters on every request and by default filters only names containing "password". Extend config :phoenix, :filter_parameters with token, secret and personal data fields, or switch to an allow-list.
  • LiveView leaks: when a socket process crashes, the triggering Phoenix.Socket.Message is logged in the termination report and the parameter filter does not apply. Implement the Inspect protocol for sensitive structs and mark Ecto fields redact: true.
  • Crash dumps: erl_crash.dump captures process state and can contain credentials. Restrict access, keep secrets out of process state where you can, or set ERL_CRASH_DUMP_SECONDS=0 in the most sensitive environments, accepting the loss of diagnostics.
  • Security events to emit: sign-in success and failure, password and email changes, session revocation, role changes, CSRF failures (Plug.CSRFProtection.InvalidCSRFTokenError) and socket origin rejections.
  • Network signals: connections to TCP 4369 or the distribution port range from outside the cluster network, and any remote or rpc session outside a change window.

Useful measures: Sobelow high and medium confidence findings open on the main branch (target zero), days from an advisory in a dependency to deployment, the share of nodes with distribution bound to a private interface or disabled (target 100%), atom count growth after startup, and the number of secrets read from compile-time configuration (target zero).

Hardening baseline

  1. Add Sobelow: {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, run mix sobelow locally, then in CI with --exit at a chosen --threshold and --format json for reporting. Start at high, triage, then lower the threshold. Record accepted false positives with --mark-skip-all and --skip so they are reviewed, not silenced. Relevant checks include Config.CSRF, Config.CSRFRoute, Config.CSWH, Config.HSTS, Config.CSP, Config.Secrets and DOS.StringToAtom. See the SAST Triage Guide.
  2. Enforce HTTPS: set force_ssl in the production endpoint (HSTS on by default) and, behind a load balancer, inject X-Forwarded-Proto and use rewrite_on so Plug sets Secure cookies and builds HTTPS URLs. Use Plug.SSL.configure or the endpoint cipher_suite option where Phoenix terminates TLS.
  3. Headers: keep put_secure_browser_headers and pass a Content Security Policy to it. See the Security Headers Guide.
  4. Sessions: set encryption_salt, use server-side session tokens, shorten lifetimes, and disconnect sockets on revocation.
  5. Secrets: move all secrets to config/runtime.exs and a secrets manager; generate secret_key_base per environment.
  6. Releases: deploy as a release, including only the OTP applications needed; releases use embedded mode, loading code at boot rather than on demand.
  7. Distribution: disable it, bind it to loopback, or run TLS distribution on a private network with EPMD bound likewise.
  8. Test and roll back: trial header and CSP changes in report-only mode, roll session changes out with a planned forced sign-in, and keep the previous release on disk so bin/my_app can be restarted on it if a change breaks production.

Common misconfigurations

  • Distribution open on all interfaces: both EPMD and distribution bind to every interface by default; on a cloud VM with a permissive security group that is remote code execution for anyone who guesses or steals the cookie.
  • Cookie in source or image: a hard-coded :cookie in mix.exs or a releases/COOKIE file baked into a public container image.
  • API pipeline reused for browser forms: moving form routes into a pipeline without protect_from_forgery to fix a token error.
  • Dynamic atoms from JSON keys: decoding with atom keys or converting parameter names to atoms to build queries.
  • Trusting signed cookies to be private: storing roles or credit decisions in the default session, which any user can decode.
  • Secrets in prod.exs: compile-time configuration copied into every artefact and image layer.

Framework mapping

AreaNIST SP 800-53 Rev. 5MITRE ATT&CK
Input handling (atoms, SQL, eval, deserialisation)SI-10T1190 Exploit Public-Facing Application; T1499.004 Application or System Exploitation
Sessions and CSRFSC-23 Session AuthenticityT1539 Steal Web Session Cookie
Distribution, remote shell and EPMDAC-3, CM-7, SC-8T1021 Remote Services
Secrets in configurationIA-5, SC-12T1552.001 Credentials In Files
Sobelow and code reviewSA-11, SA-15Supports prevention of the above
Hex dependency auditsSR-3, RA-5T1195.002 Compromise Software Supply Chain

Practitioner checklist

  • Grep for String.to_atom, binary_to_term, raw(, Repo.query, Code.eval and System.shell, and justify every hit.
  • Confirm every state-changing route is non-GET and passes through protect_from_forgery.
  • Leave check_origin on for all sockets, or use token authentication where it is off.
  • Use server-side session tokens, set encryption_salt and disconnect sockets on logout.
  • Run Sobelow, mix hex.audit and mix deps.audit in CI with failing thresholds.
  • Generate a unique secret_key_base and release cookie per environment and inject both at runtime.
  • Disable distribution or bind it and EPMD to a private network with TLS distribution.
  • Protect LiveDashboard and any admin LiveViews with authentication and an admin role check.
  • Extend filter_parameters, redact sensitive Ecto fields and control access to crash dumps.

Standards and references

Related NHI Mgmt Group resources: Application Security Guide · Secure Coding Guide · CSRF Prevention Guide · WebSocket Security Guide · Secrets Management Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 8 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org