Native ACME Support in NGINX: Reinventing TLS Automation from the Ground Up

Summary

NGINX now offers native ACME support through its ngx_http_acme_module, fundamentally transforming TLS certificate automation by integrating it directly into the web server. This solution addresses common challenges with external tools like Certbot, such as fragile dependencies and unreliable cron jobs, by embedding the ACME HTTP-01 challenge verification and certificate lifecycle management within NGINX's event loop. Developers gain significant operational reliability, a unified configuration aligning with Infrastructure as Code principles, and enhanced security due to low-privilege execution. Implementation involves compiling NGINX with the dynamic module and configuring acme_issuer, acme_certificate, and a port 80 server block for challenges directly within nginx.conf. This deep integration marks a paradigm shift towards a more robust and secure "configuration-as-everything" approach for modern infrastructures.

For any system responsible for ensuring the stability and security of live services, managing SSL/TLS certificates is a critical but challenging routine task. It’s not just a technical practice — it directly relates to user trust and data protection. With the release of the ngx_http_acme_module by NGINX, we are witnessing a fundamental shift in TLS certificate management, pushing it toward a more reliable, secure, and infrastructure as code (IaC) ideal.

Background: The Evolution of SSL/TLS Certificate Management

Before diving into NGINX’s native solution, it’s helpful to look back at the two main paths historically used in the industry to acquire and manage SSL/TLS certificates:

  • Traditional commercial certificate route: This is the oldest and classic approach. Companies or individuals pay certificate authorities (CAs) like VeriSign or GeoTrust to issue SSL certificates. The process typically involves manually generating a certificate signing request (CSR), verifying domain ownership via email or DNS records, paying, and finally deploying the certificate files to servers. The major pain points are cost (especially for many domains), tedious steps with many manual operations prone to error, and difficult renewal — if you miss renewing before expiration, services go down.

  • Rise of Let’s Encrypt and Certbot: To promote encryption everywhere, Let’s Encrypt launched, providing free, automated certificate issuance via the ACME protocol. Certbot, its most famous client, quickly became ubiquitous. It dramatically lowered the barrier for HTTPS, enabling individual developers and small businesses to deploy SSL easily. This route shifted the challenge from “getting certificates” to “maintaining automation tools.”

Challenges with Traditional Automation (Certbot)

While Certbot + cron job combinations are widely used, their architecture introduces several operational challenges:

  • Fragile external dependencies: Certbot depends on a particular runtime (e.g. Python); system updates or dependency conflicts may break it.
  • Unreliable scheduled jobs: Cron scripts can “fail silently” without clear alerts or monitoring, often only discovered when the certificate expires.
  • Configuration and execution separation: The service configuration (nginx.conf) and certificate management (scripts) are separate, violating the “single source of truth” principle, increasing the chance of human errors.
  • Permission management issues: Scripts often require elevated permissions to automate, introducing potential security risks.

ACME Core Mechanisms: Domain Ownership Verification

The ACME protocol uses a series of “challenge-response” tests to prove domain ownership. Understanding these is key to grasping automation:

HTTP-01 Challenge (core)

This is the most common method and the one supported by NGINX’s ACME module currently. The CA provides a unique “token” and asks the client (now NGINX) to place the token content at a predetermined URL path (/.well-known/acme-challenge/). The CA then fetches that URL publicly; if it matches expected content, the client is proven to control that web server. This method is simple and aligns perfectly with web servers — but requires the server’s port 80 to be publicly open.

Other challenge methods (not yet supported, but worth noting)

  • DNS-01 Challenge: Verification is done by adding a special TXT record in DNS. It supports wildcard certificates (e.g., *.example.com) and does not require exposing the server to the public internet.
  • TLS-ALPN-01 Challenge: This method uses the TLS protocol itself for verification, and does not occupy port 80. However, it’s less commonly used in practice.

NGINX ACME Module: An Architectural Solution

NGINX’s native solution embraces the HTTP-01 method, embedding verification logic into request handling and integrating the entire certificate workflow directly into NGINX — achieving simplicity and efficiency never seen before.

How to Install the NGINX ACME Module

As with many NGINX modules, ngx_http_acme_module is a dynamic module and must be compiled and loaded manually. For NGINX Plus, it can be obtained directly from official repositories. For the open source NGINX, here’s the standard process:

The nginx-acme module (and its underlying ngx-rust SDK) is new and depends on recent NGINX core API additions.

  • Minimum version: NGINX 1.25.1 — earlier versions lack necessary API and may fail compilation with errors like “not found in nginx_sys.”
  • Production recommendation: NGINX 1.28.0 or higher. This version inherits TLS optimizations from 1.26+ and enters a stable maintenance cycle focused on bug fixes — making it ideal for production.

Step 1: Prepare build environment

In addition to standard C compiler and NGINX dependencies, you need to install the Rust toolchain (cargo, rustc).

sudo apt update  
sudo apt install build-essential libpcre3-dev zlib1g-dev libssl-dev pkg-config libclang-dev git  
curl https://sh.rustup.rs | sh  
source $HOME/.cargo/env  
 

Step 2: Download sources

Fetch matching NGINX source and the nginx-acme module.

$ mkdir -pv /app/nginx/{logs,conf,cache, acme} /app/nginx-build
$ cd /app/nginx-build

# clone ACME source
$ git clone https://github.com/nginx/nginx-acme.git /app/nginx-build/nginx-acme

# download NGINX source code(choose the version you need)
wget https://nginx.org/download/nginx-1.28.0.tar.gz
tar -zxf nginx-1.28.0.tar.gz

Step 3: Build dynamic module

Configure NGINX with --add-dynamic-module pointing to the nginx-acme source. Include all existing compile flags (you can get them with nginx -V).

$ cd nginx-1.28.0
$ ./configure \
    --prefix=/app/nginx \
    --error-log-path=/app/nginx/error.log \
    --http-log-path=/app/nginx/access.log \
    --pid-path=/app/nginx/nginx.pid \
    --lock-path=/app/nginx/nginx.lock \
    --http-client-body-temp-path=/app/nginx/cache/client_temp \
    --http-proxy-temp-path=/app/nginx/cache/proxy_temp \
    --http-fastcgi-temp-path=/app/nginx/cache/fastcgi_temp \
    --http-uwsgi-temp-path=/app/nginx/cache/uwsgi_temp \
    --http-scgi-temp-path=/app/nginx/cache/scgi_temp \
    --user=nginx \
    --group=nginx \
    --with-compat \
    --with-file-aio \
    --with-threads \
    --with-http_addition_module \
    --with-http_auth_request_module \
    --with-http_dav_module \
    --with-http_flv_module \
    --with-http_gunzip_module \
    --with-http_gzip_static_module \
    --with-http_mp4_module \
    --with-http_random_index_module \
    --with-http_realip_module \
    --with-http_secure_link_module \
    --with-http_slice_module \
    --with-http_ssl_module \
    --with-http_stub_status_module \
    --with-http_sub_module \
    --with-http_v2_module \
    --with-http_v3_module \
    --with-mail \
    --with-mail_ssl_module \
    --with-stream \
    --with-stream_realip_module \
    --with-stream_ssl_module \
    --with-stream_ssl_preread_module \
    --with-cc-opt='-g -O2 -ffile-prefix-map=/home/builder/debuild/nginx-1.28.0/debian/debuild-base/nginx-1.28.0=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' \
    --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' \
    --add-dynamic-module=/app/nginx-build/nginx-acme

$ make && \
    make modules && \
    make install

# Run the configuration script — the key option here is --add-dynamic-module
# Note: you must include all of your current NGINX compile flags here (you can check them via nginx -V)
# Compile the module — be sure to run make modules instead of make install

Step 4: Load and enable module

After successful compilation, a .so file will be generated under the objs directory. You need to copy it into NGINX’s module directory. If you ran make install as above, manual copying may not be necessary.

Below is a fully functional nginx.conf file:

# /app/nginx/conf/nginx.conf
user nginx;
error_log  error.log  debug;
pid        nginx.pid;

load_module modules/ngx_http_acme_module.so;

events {
    worker_connections  1024;
    multi_accept on;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$host" "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  access.log  main;
    sendfile       on;
    tcp_nopush     on;
    charset utf-8;
    keepalive_timeout  65;
    gzip  on;

    resolver 8.8.8.8 1.1.1.1;
    acme_issuer letsencrypt {
        uri         https://acme-v02.api.letsencrypt.org/directory;
        contact     mailto:[email protected];
        state_path  acme/letsencrypt;
        accept_terms_of_service;
    }
    acme_shared_zone zone=acme_shared:1M;

    server {
        listen 443 ssl;
        server_name ssl.aidig.co;

        acme_certificate    letsencrypt;
        ssl_certificate     $acme_certificate;
        ssl_certificate_key   $acme_certificate_key;
        ssl_certificate_cache   max=2;  # required ngx 1.27.4+

        location / {
            default_type text/plain;
            return 200 'OK';
        }
    }

    server {
        listen 80 default_server;
        server_name _;

        location / {
            return 301 https://$host$request_uri;
        }
    }
}

After completing the steps above and restarting NGINX, the ACME module will be loaded and ready.

# Validate configuration syntax
$ cd /app/nginx/
$ ./sbin/nginx -c conf/nginx.conf -t

# Start
$ ./sbin/nginx -c conf/nginx.conf

# Reload after config changes
$ ./sbin/nginx -c conf/nginx.conf -s reload

Core Mechanism & Configuration Details of Ngx-ACME

Before showing the specific configuration for the ACME module, it’s important to emphasize: a full HTTPS setup in production should include various TLS optimization directives for performance and security (for example, ssl_session_cache / ssl_session_timeout for session reuse to reduce latency; ssl_protocols and ssl_ciphers to define supported secure protocols and cipher suites).

To keep this article focused on the core topic, the configuration shown below includes only the minimal settings necessary to implement ACME automation.

The whole flow is simplified into pure NGINX configuration directives — intuitive and powerful.

Define ACME Issuer (acme_issuer)

http {
    resolver 127.0.0.1:53;
    # Optional directive acme_shared_zone, used to store certificates, private keys, and challenge data for all configured issuers. The default zone size is 256K, which you can increase if needed.
    acme_shared_zone zone=acme_shared:1M;
    # Define an ACME issuer instance named letsencrypt_prod
    acme_issuer letsencrypt {
        # Specify the directory URL of the ACME provider; here it’s Let's Encrypt’s production environment
        uri         https://acme-v02.api.letsencrypt.org/directory;
        # Provide a contact email address to receive important CA notifications (e.g., certificate expiration)
        contact     mailto:[email protected];
        # Agree to terms of service — required when using Let's Encrypt or similar CAs
        accept_terms_of_service;
        # Specify the state path for storing the ACME account key (very important)
        state_path  acme/letsencrypt;
    }
}

Declare and Apply ACME Certificates in a server Block

In a server block where HTTPS is needed, you use acme_certificate to declare that certificate issuance is needed, and immediately use dynamic variables from the module to apply them:

server {
    listen 443 ssl;
    server_name www.example.com;

    # Step 1: Declare that this server block enables ACME and specify the previously defined issuer
    acme_certificate letsencrypt;

    # Step 2: Use dynamic variables to load the certificate and private key managed by the ACME module in memory
    ssl_certificate     $acme_certificate;
    ssl_certificate_key $acme_certificate_key;

    ssl_certificate_cache max=2;  # ngx 1.27.4+

    location / {
        default_type text/plain;
        return 200 "OK";
    }
}

Configure the HTTP-01 Challenge Response Endpoint

server {
    # Listen on port 80 as the default server, capturing all HTTP requests
    listen 80 default_server;
    # Use a wildcard server name to match any host not explicitly matched by other server blocks
    server_name _;

    # The ACME module will handle `/.well-known/acme-challenge/` requests automatically; this `location` is for all other requests
    location / {
        # Redirect all non-ACME HTTP traffic to HTTPS
        return 301 https://$host$request_uri;
    }
}

Conclusion: A Fundamental Shift in Workflow

The introduction of native ACME support in NGINX marks a major advancement in TLS automation management. It is no longer just an incremental improvement on existing workflows, but a complete paradigm shift — transforming certificate management from a fragile external dependency (like Certbot + cron) into a core capability of NGINX itself that addresses many traditional pain points.

This level of deep integration brings:

  • Operational reliability: replacing unreliable scheduled jobs with NGINX’s mature event loop eliminates risks of silent failures.
  • Unified configuration: embedding certificate lifecycle logic directly in nginx.conf makes it the single source of truth, aligning perfectly with IaC philosophy.
  • Security: the entire workflow runs under NGINX’s standard process with low privileges, avoiding unnecessary privilege escalation and narrowing the attack surface.

For modern infrastructures that pursue high automation and stability, this is undoubtedly a meaningful step toward a “configuration-as-everything” future.

References

Note the post is translated from https://sconts.com/post/nginx-native-acme-support/

NGINX HTTPS AUTOMATIVE RENEW

  RELATED

  COMMENTS

0

No comment for this article.