The Erlang/OTP inets application provides an HTTP client (httpc) and an
HTTP server (httpd), intended to be used as libraries in other applications.
Different deployments have very different security requirements. A development
tool running on localhost has different needs than a service exposed to the
public internet. Because of this, inets ships with permissive defaults that
prioritize ease of use. This guide describes how to tighten those defaults for
production deployments.
This guide targets OTP 29. Where behavior differs from OTP 28 and earlier, the differences are noted inline. A summary of changes is provided in the Changes from OTP 28 section.
For general configuration, see the m:httpc and m:httpd reference manuals.
Since OTP 26.0, httpc verifies the server certificate by default when no
{ssl, ...} option is given. The default is equivalent to calling
httpc:ssl_verify_host_options(true), which enables verify_peer with the
system CA store.
If you need to customize TLS settings (for example, to pin a specific CA or restrict protocol versions), pass them explicitly:
SslOpts = [{verify, verify_peer},
{cacertfile, "/path/to/ca-bundle.crt"},
{versions, ['tlsv1.2', 'tlsv1.3']}],
httpc:request(get, {"https://example.com", []}, [{ssl, SslOpts}], []).Setting
{ssl, [{verify, verify_none}]}disables certificate verification entirely. This makes the connection vulnerable to man-in-the-middle attacks and should only be used for testing.
The default value for both timeout and connect_timeout is infinity,
meaning a request can hang indefinitely. Always set explicit timeouts in
production:
HttpOpts = [{timeout, 30000}, %% 30 s total request timeout
{connect_timeout, 5000}], %% 5 s TCP connect timeout
httpc:request(get, {"https://example.com", []}, HttpOpts, []).-
timeout- Maximum time for the entire request (connect + send + receive). Default:infinity. -
connect_timeout- Maximum time for the TCP connection setup. Defaults to the value oftimeout. -
autoretry(since OTP 28.4) - Controls how long the client honors a server'sRetry-Afterheader before retrying. The client automatically retries the request once on a 503 response with aRetry-Afterheader. Default:infinity(always honor the server's value). Set to0to disable automatic retries, or to a finite number of milliseconds to cap the wait. If the server'sRetry-Aftervalue exceeds the configured limit, no retry is performed. Before OTP 28.4, the retry behavior was hardcoded to only honorRetry-Aftervalues of 99 seconds or less.HttpOpts = [{timeout, 30000}, {connect_timeout, 5000}, {autoretry, 10000}]. %% Wait at most 10 s for Retry-After
By default, httpc follows HTTP redirects automatically
({autoredirect, true}). This can be exploited for Server-Side Request
Forgery (SSRF) if the client is used to fetch user-supplied URLs, because a
redirect can point to internal hosts or services.
For requests to untrusted URLs, disable automatic redirects and validate the target manually. Check that the redirect target does not resolve to a private network address (10.x, 127.x, 169.254.x, etc.) before following it:
httpc:request(get, {UserUrl, []},
[{autoredirect, false}, {timeout, 10000}], []).Profile-level options control how many connections httpc keeps open. The
per-host defaults are conservative, but the global limit defaults to
infinity:
-
max_connections_open(since OTP 29) - Maximum total number of open handlers across all hosts. Default:infinity. Set a finite value to prevent unbounded resource consumption:httpc:set_options([{max_connections_open, 100}]).
-
max_sessions- Maximum persistent connections per host:port. Default:2. -
max_keep_alive_length- Maximum queued requests on a keep-alive connection. Default:5. -
max_pipeline_length- Maximum pipelined requests per connection. Default:2. -
pipeline_timeout- Idle time before closing a pipelined connection, in milliseconds. Default:0(pipelining disabled). Pipelining is only used when this is set to a positive value. -
keep_alive_timeout- Idle time before closing a persistent connection. Default:120000ms (2 minutes).
For a service making requests to many different hosts, set
max_connections_open to a value appropriate for your system's file
descriptor limits.
Cookies are disabled by default ({cookies, disabled}). If you enable them,
prefer verify mode, which lets you inspect cookies before they are stored
via httpc:store_cookies/3:
httpc:set_options([{cookies, verify}]).When using a proxy, ensure that the no_proxy list excludes internal hosts
that should not be routed through the proxy:
httpc:set_options([
{proxy, {{"proxy.example.com", 8080},
["localhost", "*.internal.example.com"]}}
]).For HTTPS traffic through a proxy, configure https_proxy separately. The
client uses the HTTP CONNECT method to establish a tunnel through the proxy.
By default, https_proxy inherits the value of proxy.
The following options can introduce injection, file overwrite, or data leakage vulnerabilities if misused.
Per-request options:
-
{socket_opts, Opts}- Passed directly to the transport layer without validation. Avoid exposing this to untrusted input. -
{headers_as_is, true}- Bypasses header normalization. This can enable header injection if header values come from untrusted sources. -
{stream, {self, once}}or{stream, Filename}- Streaming to a file path could overwrite files. Validate paths before use.
Per-profile options:
{verbose, debug | trace}- May log sensitive data such as headers containing authorization tokens.
By default, httpd listens in plaintext ({socket_type, ip_comm}). For any
deployment beyond localhost, enable TLS:
httpd:start_service([{port, 8443},
{server_root, "/var/www"},
{document_root, "/var/www/htdocs"},
{socket_type, {ssl, [{cert_keys, [#{certfile => "/path/to/cert.pem",
keyfile => "/path/to/key.pem"}]},
{versions, ['tlsv1.2', 'tlsv1.3']}]}}]).The default {bind_address, any} listens on all network interfaces. Restrict
this to the intended interface:
{bind_address, {127, 0, 0, 1}} %% Localhost onlyor
{bind_address, "192.168.1.10"} %% Specific interfaceBefore OTP 30 several size limits default to nolimit, which allows arbitrarily large
requests that can exhaust memory. Set explicit limits:
[{max_uri_size, 8192}, %% 8 KB URI limit (default since OTP 30)
{max_header_size, 10240}, %% 10 KB (already the default)
{max_body_size, 10_485_760}, %% 10 MB body limit
{max_content_length, 10_485_760}] %% 10 MB Content-Length check-
max_uri_size- Maximum URI length in bytes. Default:8192(8 KB). -
max_header_size- Maximum total header size. Default:10240(10 KB). -
max_body_size- Maximum received body size during parsing. Default:100_000_000(100 MB). -
max_content_length- Rejects requests whoseContent-Lengthheader exceeds this value with a 413 response, before reading the body. Default:100_000_000(100 MB). -
max_client_body_chunk- When handling large PUT or POST bodies viamod_esi, setting this option enforces chunked delivery to the ESI callback. This prevents the server from buffering the entire request body in memory, which could be exploited to cause memory exhaustion. Default:nolimit.
max_body_sizeandmax_content_lengthserve different purposes.max_content_lengthis a fast pre-check on the header value.max_body_sizelimits the actual bytes received. Set both for defense in depth.
-
max_clients- Maximum simultaneous connections. Default:150. Tune this to your expected load and available resources. -
keep_alive- Controls whether persistent connections are used. Default:true. Disabling persistent connections (false) eliminates certain classes of connection-reuse attacks at the cost of performance. -
max_keep_alive_request- Maximum requests per persistent connection. Default:infinity. Set a finite value to prevent a single connection from monopolizing server resources:{max_keep_alive_request, 1000} -
keep_alive_timeout- Seconds before closing an idle persistent connection. Default:150. -
minimum_bytes_per_second- Closes connections that transfer data below this rate. Not set by default. Enable it to mitigate slow-rate DoS attacks (for example, Slowloris):{minimum_bytes_per_second, 100}
The following table summarizes when each limit is checked during the request lifecycle:
| Stage | Event | Limit checked |
|---|---|---|
| 1 | TCP connected | max_clients |
| 2 | Headers received | max_uri_size, max_header_size |
| 3 | Content-Length checked | max_content_length |
| 4 | Body received | max_body_size |
| 5 | Idle on keep-alive | keep_alive_timeout |
| 6 | Slow transfer | minimum_bytes_per_second |
The default {server_tokens, minimal} reveals the complete inets version
string in the Server response header (for example, inets/9.3.1). Despite
the name, minimal still includes the full version number. This helps
attackers identify known vulnerabilities. Reduce information disclosure:
{server_tokens, none} %% Omit the Server header entirelyor
{server_tokens, prod} %% Just "inets", no versionThe {modules, ...} option controls which httpd modules are active. In OTP 29,
mod_cgi and mod_actions are deprecated and removed from the default module
list. The default is:
[mod_alias, mod_auth, mod_esi,
mod_dir, mod_get, mod_head, mod_log, mod_disk_log]In OTP 28 and earlier, the default module list also includes
mod_cgiandmod_actions, enabling CGI execution out of the box. If you are running an older release, removing these modules manually is especially important.
Review this list and remove modules you do not need:
-
mod_cgiandmod_actions- Enable CGI script execution. Deprecated in OTP 29 and scheduled for removal in OTP 30. If you still need CGI support, add them explicitly to your module list. CGI introduces a large attack surface (arbitrary process execution, environment variable injection). -
mod_dir- Enables directory listing. Remove it to prevent information disclosure about your file structure. -
mod_esi- Enables Erlang Scripting Interface. If used, restrict it witherl_script_aliasto a whitelist of allowed modules. -
mod_trace- Handles HTTP TRACE requests. TRACE can be exploited in cross-site tracing (XST) attacks. This module is not in the default list but should never be added in production.
A minimal module chain for a static file server:
{modules, [mod_alias, mod_get, mod_head, mod_log]}If using mod_auth:
-
Avoid
{auth_type, plain}in production. It stores passwords in cleartext files. Preferdetsormnesia. -
Place auth files outside
document_root. Theauth_user_fileandauth_group_filemust not be accessible via HTTP. -
Set
auth_access_passwordto a strong value. When not set or set to"NoPassword", no password is required for the authentication management API. -
Use IP-based restrictions (
allow_from,deny_from) as an additional layer, not as the sole access control mechanism.
The
mod_authmodule implements HTTP Basic Authentication, which transmits credentials in base64 encoding (effectively cleartext). Always use TLS when authentication is enabled.
mod_security acts as a filter on top of mod_auth, tracking failed login
attempts per user and temporarily blocking users who exceed a threshold. This
mitigates credential-stuffing and brute-force password guessing attacks.
Enable mod_security to throttle authentication brute force attempts:
{security_directory, {"/protected", [
{data_file, "/var/lib/httpd/security.dat"},
{max_retries, 3},
{block_time, 60}, %% Block for 60 minutes
{fail_expire_time, 30}, %% Remember failures for 30 minutes
{auth_timeout, 30}
]}}-
data_file- Path to the persistent security data file. Store this outsidedocument_root. Required formod_securityto persist blocked-user state across server restarts. -
max_retries- Maximum failed authentication attempts before the user is blocked. Default:3. -
block_time- Minutes a blocked user remains locked out. Default:60. -
fail_expire_time- Minutes before a failed attempt is forgotten. If the user does not retry within this window, the failure counter resets. Default:30. -
auth_timeout- Seconds a successful authentication is remembered. After expiry the user must re-authenticate. Default:30.
mod_securitymust appear aftermod_authin the module chain. It relies onmod_authto perform the actual authentication;mod_securityonly observes the results and enforces blocking policy.
For runtime inspection and manual blocking, see m:mod_security
(list_blocked_users/1, block_user/5, unblock_user/4).
-
script_aliasmaps URL paths to CGI script directories. Ensure the mapped directory contains only intended scripts and is not writable by the web server process. -
erl_script_aliascontrols which Erlang modules can be called via ESI. Always specify an explicit whitelist:{erl_script_alias, {"/esi", [my_allowed_module]}}Never use a wildcard or overly broad module list.
-
script_timeoutanderl_script_timeoutdefault to 15 seconds. Review whether this is appropriate for your use case. -
script_nocacheanderl_script_nocache- When set totrue, the server adds HTTP header fields preventing proxies from caching dynamic responses. Default:false. Enable these to prevent stale or sensitive dynamic content from being served from proxy caches.
The
script_aliaspath resolution can bypassmod_authdirectory protections depending on module ordering. Ensuremod_authappears beforemod_cgiin the module chain, and test that authentication is enforced on CGI paths.
Enable logging to detect and investigate security incidents:
[{error_log, "/var/log/httpd/error.log"},
{security_log, "/var/log/httpd/security.log"},
{transfer_log, "/var/log/httpd/access.log"},
{log_format, combined}] %% Includes referer and user-agent (default: common)For production systems, the m:disk_log variants (transfer_disk_log,
error_disk_log, security_disk_log) are recommended as they support wrap
logs with configurable size limits, preventing log files from consuming all
available disk space.
For integration with the OTP logger framework, where my_httpd is the
ServerID atom used in the logger domain hierarchy
[otp, inets, httpd, ServerID, error]:
{logger, [{error, my_httpd}]}Ensure log files are rotated and that log directories are not writable by the web server process.
The following is a complete example combining the recommendations above for an httpd deployment serving static files with TLS:
httpd:start_service([
{port, 8443},
{bind_address, {127, 0, 0, 1}},
{server_root, "/var/www"},
{document_root, "/var/www/htdocs"},
{server_tokens, none},
{socket_type, {ssl, [{cert_keys, [#{certfile => "/path/to/cert.pem",
keyfile => "/path/to/key.pem"}]},
{versions, ['tlsv1.2', 'tlsv1.3']}]}},
{modules, [mod_alias, mod_auth, mod_security, mod_get, mod_head, mod_log]},
{max_clients, 100},
{max_keep_alive_request, 1000},
{keep_alive_timeout, 60},
{max_uri_size, 8192},
{max_header_size, 10240},
{max_body_size, 10_485_760},
{max_content_length, 10_485_760},
{minimum_bytes_per_second, 100},
{error_log, "/var/log/httpd/error.log"},
{transfer_log, "/var/log/httpd/access.log"},
{log_format, combined}
]).And a hardened httpc request:
HttpOpts = [{timeout, 30000},
{connect_timeout, 5000},
{autoredirect, false},
{autoretry, 10000},
{ssl, [{verify, verify_peer},
{cacerts, public_key:cacerts_get()},
{versions, ['tlsv1.2', 'tlsv1.3']}]}],
httpc:request(get, {"https://example.com", []}, HttpOpts, []).The inets application provides application-level security controls. For
production deployments, combine these with:
- Network-level controls - Firewalls, network segmentation, and rate limiting.
- OS-level controls - Run the BEAM VM as an unprivileged user. Use file system permissions to protect configuration files, log files, and credential stores.
- Reverse proxy - Consider placing
httpdbehind a dedicated reverse proxy (such as Nginx or HAProxy) that provides additional request filtering, rate limiting, and TLS termination.
Use the OTP m:logger framework to monitor for:
- Repeated authentication failures (potential brute-force attacks).
- Unusual request patterns (potential scanning or fuzzing).
- Connection limit exhaustion (potential DoS).
Security vulnerabilities are fixed in new releases of Erlang/OTP. Monitor the Erlang/OTP releases and apply updates promptly.
If you are upgrading from OTP 28 or running an older release, be aware of the following differences:
-
autoretrynot available before OTP 28.4 - The{autoretry, timeout()}option forhttpcwas introduced in OTP 28.4. On older releases, the client still retries on 503 withRetry-After, but only honors values of 99 seconds or less (hardcoded). The new option allows configuring this limit or disabling retries entirely with{autoretry, 0}. -
max_connections_opennot available before OTP 29 - The{max_connections_open, integer()}profile option forhttpcwas introduced in OTP 29. On older releases, there is no global limit on the number of open handlers. -
TLS verification default -
httpchas verified server certificates by default since OTP 26.0. On releases before 26.0, you must pass{ssl, httpc:ssl_verify_host_options(true)}explicitly or connections will proceed without certificate verification.
mod_cgiandmod_actionsin default modules - In OTP 28 and earlier, the default{modules, ...}list includesmod_cgiandmod_actions, enabling CGI execution by default. In OTP 29 these modules are deprecated and removed from the defaults. On older releases, remove them explicitly.