Public façade for the break_glass_ex emergency access library.
This module is the primary entry point for all host-application controllers.
Internal modules (BreakGlass.RateLimiter, BreakGlass.OtpStore, etc.) are
considered private to the library.
Purpose
break_glass_ex provides an in-process emergency access subsystem for Elixir
and Phoenix applications. When all normal admin accounts are locked out, a
pre-configured break-glass credential can be used to regain access through a
two-factor authentication flow (password → emailed OTP).
Installation
Add break_glass_ex to your mix.exs dependencies:
{:break_glass_ex, "~> 0.1"}Configuration
All configuration lives under config :break_glass_ex in your runtime.exs:
config :break_glass_ex,
email: System.fetch_env!("BREAK_GLASS_EMAIL"),
password_hash: System.fetch_env!("BREAK_GLASS_PASSWORD_HASH"),
user_provider: MyApp.BreakGlassUserProvider,
allowed_ips: ~w[10.0.0.0/8 127.0.0.1],
alert_emails: ["security@example.com"],
mailer: MyApp.MailerSee the Configuration Reference in README.md for the full list of keys.
Supervision Tree Setup
Add BreakGlass.Supervisor to your application's supervision tree before
your endpoint or router:
children = [
# ... other children ...
{BreakGlass.Supervisor, []},
MyAppWeb.Endpoint
]UserProvider Behaviour
Implement BreakGlass.UserProvider in your host application to return your
own user struct on successful break-glass authentication:
defmodule MyApp.BreakGlassUserProvider do
@behaviour BreakGlass.UserProvider
def build_user(attrs) do
%MyApp.User{
id: attrs.sentinel_id,
email: attrs.email,
break_glass: true
}
end
endSecurity Considerations
- Physical IP only. Always pass
conn.remote_ip(formatted via:inet.ntoa/1) and never derive the IP fromx-forwarded-foror any proxy header. x-forwarded-forWARNING. The library has no way to enforce IP origin. It is a documented contract that the host controller MUST use the physical IP.- Password hash storage. Never store the plaintext break-glass password in
source control. Use
mix break_glass.gen_hash <password>to generate a hash and store it in an environment variable or secrets manager. - Lockout defaults. The default rate limit is 5 attempts with a 900-second
(15 minute) lockout window. Configure
:max_attemptsand:lockout_secondsto suit your security policy.
Summary
Functions
Returns true if the given user term has break_glass: true.
Step 1 of the two-factor authentication flow: verify email, IP, and password.
Returns a list containing the configured break-glass email address.
Returns true if ip is in the configured IP whitelist.
Returns true if ip is currently locked out due to too many failed attempts.
Returns the number of remaining authentication attempts for ip.
Returns the configured sentinel ID (default: 0).
Step 2 of the two-factor authentication flow: verify the OTP code.
Functions
Returns true if the given user term has break_glass: true.
Handles both plain maps with atom keys and structs that implement the
break_glass field.
iex> BreakGlass.active?(%{break_glass: true})
true
iex> BreakGlass.active?(%{break_glass: false})
false
iex> BreakGlass.active?(%{})
false
@spec authenticate(email :: String.t(), password :: String.t(), ip :: String.t()) :: {:ok, :otp_required} | {:error, :ip_not_allowed} | {:error, :rate_limited} | :error
Step 1 of the two-factor authentication flow: verify email, IP, and password.
Arguments
email— the email address submitted by the operatorpassword— the plaintext password to verify against the configured bcrypt haship— the physical source IP address (fromconn.remote_ip, never from headers)
Returns
{:ok, :otp_required}— credentials correct; OTP sent; host should redirect to OTP form{:error, :rate_limited}— IP is currently locked out after too many failed attempts{:error, :ip_not_allowed}— IP is not in the configured whitelist:error— email or password did not match (intentionally ambiguous)
All rejection paths call Bcrypt.no_user_verify/0 to maintain constant-time response
latency regardless of which check fails.
@spec emails() :: [String.t()]
Returns a list containing the configured break-glass email address.
Returns [email] where email is read from config :break_glass_ex, :email.
Returns true if ip is in the configured IP whitelist.
The whitelist is read from config :break_glass_ex, :allowed_ips and defaults
to ["127.0.0.1", "::1"] if the key is absent. Each entry may be an exact IP
address or a CIDR range (e.g. "10.0.0.0/8").
Host-app controllers can call this function directly before invoking
authenticate/3 to short-circuit a request early and return a proper HTTP
response.
Returns true if ip is currently locked out due to too many failed attempts.
Delegates to BreakGlass.RateLimiter.locked_out?/1.
@spec remaining_attempts(ip :: String.t()) :: non_neg_integer()
Returns the number of remaining authentication attempts for ip.
Delegates to BreakGlass.RateLimiter.remaining_attempts/1.
@spec sentinel_id() :: non_neg_integer()
Returns the configured sentinel ID (default: 0).
The sentinel ID is a value that can never match a real database primary key, used as the break-glass user's identifier to prevent accidental DB writes.
@spec verify_otp(code :: String.t(), ip :: String.t()) :: {:ok, user :: term()} | {:error, :invalid_otp}
Step 2 of the two-factor authentication flow: verify the OTP code.
Arguments
code— the 6-digit OTP string submitted by the operatorip— the physical source IP address (must match the IP used in step 1)
Returns
{:ok, user}— OTP verified;useris the term returned by the configuredUserProvider.build_user/1callback{:error, :invalid_otp}— code did not match, had expired, or no OTP was pending
On success, the rate limit counter for ip is reset and a Logger.warning is emitted.
On failure, a Logger.warning is emitted with the IP and failure reason.