Introduction

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 httpc and httpd reference manuals.

HTTP Client (httpc)

TLS Configuration

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}], []).

Warning

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.

Timeouts

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 of timeout.

  • autoretry (since OTP 28.4) - Controls how long the client honors a server's Retry-After header before retrying. The client automatically retries the request once on a 503 response with a Retry-After header. Default: infinity (always honor the server's value). Set to 0 to disable automatic retries, or to a finite number of milliseconds to cap the wait. If the server's Retry-After value exceeds the configured limit, no retry is performed. Before OTP 28.4, the retry behavior was hardcoded to only honor Retry-After values of 99 seconds or less.

    HttpOpts = [{timeout, 30000},
                {connect_timeout, 5000},
                {autoretry, 10000}].  %% Wait at most 10 s for Retry-After

Redirect Following

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}], []).

Client Connection Limits

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: 120000 ms (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}]).

Proxy Configuration

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.

Sensitive Options

Warning

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:

HTTP Server (httpd)

Enable TLS

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']}]}}]).

Bind Address

The default {bind_address, any} listens on all network interfaces. Restrict this to the intended interface:

{bind_address, {127, 0, 0, 1}}   %% Localhost only

or

{bind_address, "192.168.1.10"}    %% Specific interface

Request Size Limits

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
 {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: nolimit.

  • max_header_size - Maximum total header size. Default: 10240 (10 KB).

  • max_body_size - Maximum received body size during parsing. Default: nolimit.

  • max_content_length - Rejects requests whose Content-Length header 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 via mod_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.

Note

max_body_size and max_content_length serve different purposes. max_content_length is a fast pre-check on the header value. max_body_size limits the actual bytes received. Set both for defense in depth.

Server Connection Limits

  • 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:

StageEventLimit checked
1TCP connectedmax_clients
2Headers receivedmax_uri_size, max_header_size
3Content-Length checkedmax_content_length
4Body receivedmax_body_size
5Idle on keep-alivekeep_alive_timeout
6Slow transferminimum_bytes_per_second

Server Identity

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 entirely

or

{server_tokens, prod}   %% Just "inets", no version

Module Chain

The {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]

Warning

In OTP 28 and earlier, the default module list also includes mod_cgi and mod_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_cgi and mod_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 with erl_script_alias to 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]}

Authentication (mod_auth)

If using mod_auth:

  • Avoid {auth_type, plain} in production. It stores passwords in cleartext files. Prefer dets or mnesia.

  • Place auth files outside document_root. The auth_user_file and auth_group_file must not be accessible via HTTP.

  • Set auth_access_password to 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.

Warning

The mod_auth module implements HTTP Basic Authentication, which transmits credentials in base64 encoding (effectively cleartext). Always use TLS when authentication is enabled.

Brute Force Protection (mod_security)

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 outside document_root. Required for mod_security to 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.

Warning

mod_security must appear after mod_auth in the module chain. It relies on mod_auth to perform the actual authentication; mod_security only observes the results and enforces blocking policy.

For runtime inspection and manual blocking, see mod_security (list_blocked_users/1, block_user/5, unblock_user/4).

CGI and ESI Execution

  • script_alias maps 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_alias controls 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_timeout and erl_script_timeout default to 15 seconds. Review whether this is appropriate for your use case.

  • script_nocache and erl_script_nocache - When set to true, 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.

Warning

The script_alias path resolution can bypass mod_auth directory protections depending on module ordering. Ensure mod_auth appears before mod_cgi in the module chain, and test that authentication is enforced on CGI paths.

Logging

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 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.

Hardened Example

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, []).

General Recommendations

Defense in Depth

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 httpd behind a dedicated reverse proxy (such as Nginx or HAProxy) that provides additional request filtering, rate limiting, and TLS termination.

Monitoring

Use the OTP logger framework to monitor for:

  • Repeated authentication failures (potential brute-force attacks).
  • Unusual request patterns (potential scanning or fuzzing).
  • Connection limit exhaustion (potential DoS).

Keep Up to Date

Security vulnerabilities are fixed in new releases of Erlang/OTP. Monitor the Erlang/OTP releases and apply updates promptly.

Changes from OTP 28

If you are upgrading from OTP 28 or running an older release, be aware of the following differences:

httpc

  • autoretry not available before OTP 28.4 - The {autoretry, timeout()} option for httpc was introduced in OTP 28.4. On older releases, the client still retries on 503 with Retry-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_open not available before OTP 29 - The {max_connections_open, integer()} profile option for httpc was introduced in OTP 29. On older releases, there is no global limit on the number of open handlers.

  • TLS verification default - httpc has 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.

httpd

  • mod_cgi and mod_actions in default modules - In OTP 28 and earlier, the default {modules, ...} list includes mod_cgi and mod_actions, enabling CGI execution by default. In OTP 29 these modules are deprecated and removed from the defaults. On older releases, remove them explicitly.