IpAccessControl (IP Access Control Plug v1.1.0)
View SourceThis Plug restricts requests so that they must come from the range of IP addresses
specified in the pipeline config. A request's IP address is deemed to be present as
%Plug.Conn{remote_ip: _}.
If the request IP is not allowed, the specified response code and body will be added to the Plug.Conn and the chain will be halted. Otherwise, the plug chain will continue.
Include this module in your plug chain with its configuration.
Configuration
There are two main configuration options:
the allow list of IP addresses or CIDR ranges, which may be configured as a static list or as a function returning the list of IP addresses or CIDR ranges; and
a Plug (either module or function) to call when the remote IP address is not allowed.
Note that each item in the allow list must be tested in turn, so a smaller list will outperform a larger list. Future versions of this Plug may include a way of caching results.
Allow List Configuration
The list of permitted IP addresses or CIDR ranges may be specified using either the
module option described below or the allow parameter.
The allow parameter must be one of the following:
- a list of IP addresses or CIDR ranges, or
- a 0-arity function that returns a list of IP addresses or CIDR ranges, or
- a
{module, function}tuple to a 0-arity function that returns a list of IP addresses or CIDR ranges.
Formats supported include:
- IPv4 string format;
- IPv6 string format;
- CIDRv4 string format; or
- CIDRv6 string format.
Examples:
# Include after a plug which puts the request IP to the remote_ip
# attribute on the Plug.Conn.
plug IpAccessControl, allow: ["1.1.1.0/31", "1.1.0.0/24", "127.0.0.0/8"]
def allow, do: ["1.1.1.0/31", "1.1.0.0/24", "127.0.0.0/8"]
plug IpAccessControl, allow: &__MODULE__.allow/0Blocked Action Configuration
The action to take when the remote IP address is not allowed may be specified using the
module option described below or the on_blocked option. If not specified, a default
on_blocked implementation will be provided that uses response_code_on_blocked and
response_body_on_blocked.
When the remote IP address is blocked, the Plug pipeline is halted.
on_blocked: A Plug that will be called when the IP address is not allowed. It will be passed the options provided to the the IpAccessControl plug.response_code_on_blocked: The HTTP status code assigned to the response when the request's IP address is not allowed. Defaults to401if not specified.response_body_on_blocked: The body assigned to the response when the request's IP address is not allowed. Defaults to"Not Authenticated"if not specified.
Example:
def on_blocked(conn, opts) do
Conn.send_resp(
conn,
options[:response_code_on_blocked],
String.reverse(options[:response_body_on_blocked])
)
end
plug IpAccessControl,
allow: ["1.1.1.0/31", "1.1.0.0/24", "127.0.0.0/8"],
on_blocked: &__MODULE__.on_blocked/2Module Configuration
A single configuration option can be provided as module that refers to a module that
implements one or both of the functions ip_access_allow_list/0 (this is a function
that will be used for allow) and ip_access_on_blocked/2 (this is a Plug function
used for on_blocked).
If provided, the configurations available through module will take priority over
functions or values specified in allow or on_blocked.
The IpAccessControl can be configured with any of the following options.
Example:
plug IpAccessControl, module: EmployeeAccessInstallation
Add ip_access_control to your dependencies. If your application is running behind
a proxy, you will probably need to also include remote_ip or a similar "Real" IP
discovery library as a dependency.
def deps do
{:ip_access_control, "~> 1.0"},
{:remote_ip, "~> 1.0"} # Required if behind a proxy
endSecurity Note
Adam Pritchard wrote an extensive post on the perils of discovering the "real" client IP. Because this module is intended for security, you should be behind a proxy that you control or trust and reading the rightmost IP in the derived remote IP address list.
The key points he makes are:
- When deriving the "real client IP address" from the
X-Forwarded-Forheader, use the rightmost IP in the list.- The leftmost IP in the XFF header is commonly considered to be "closest to the client" and "most real", but it's trivially spoofable. Don't use it for anything even close to security-related.
- When choosing the rightmost XFF IP, make sure to use the last instance of that header.
- Using special "true client IPs" set by reverse proxies (like
X-Real-IP,True-Client-IP, etc.) can be good, but it depends on a) how the reverse proxy actually sets it, b) whether the reverse proxy sets it if it's already present/spoofed, and c) how you've configured the reverse proxy (sometimes).- Any header not specifically set by your reverse proxy cannot be trusted. For example, you must not check the
X-Real-IPheader if you're not behind Nginx or something else that always sets it, because you'll be reading a spoofed value.- A lot of rate limiter implementations are using spoofable IPs and are vulnerable to rate limiter escape and memory exhaustion attacks.
If you use the "real client IP" anywhere in your code or infrastructure, you need to go check right now how you're deriving it.
Summary
Functions
Returns true if the remote IP is in the given allow list. The remote IP
address can be provided either as a Plug.Conn.t(), an IP address tuple, or
an IP address string.
Callback implementation for Plug.call/2.
Initialize the plug with options.
Functions
@spec allowed?( Plug.Conn.t() | binary() | :inet.ip_address() | nil | BitwiseIp.t(), [binary(), ...] | (-> [binary(), ...]) | ip_block_list() | nil ) :: boolean()
Returns true if the remote IP is in the given allow list. The remote IP
address can be provided either as a Plug.Conn.t(), an IP address tuple, or
an IP address string.
If the remote IP is provided as a Plug.Conn.t(), the remote IP will be
pulled from the Plug.Conn.t()'s remote_ip. If the remote IP is provided
as a string, this function will return false if the IP address cannot be
parsed.
If neither the remote_ip nor allow list are provided, always returns
false.
@spec call(Plug.Conn.t(), IpAccessControl.Options.config()) :: Plug.Conn.t()
Callback implementation for Plug.call/2.
@spec init(IpAccessControl.Options.input_config()) :: IpAccessControl.Options.config()
Initialize the plug with options.
@spec ip_access_on_blocked(Plug.Conn.t(), IpAccessControl.Options.config()) :: Plug.Conn.t()