Configuration-based, opinionated SNI-based nginx reverse proxy with automatic Let's Encrypt certificates and optional client certificate (mTLS) authentication.
Scope and Features
This repo is meant to provide an easy-to-setup, internet-facing reverse-proxy for private networks.
The typical use is to secure services in your home network while making them reachable from the general internet.
Your system is fully described in a JSON or YAML configuration file (see config.example.json, config.example.yaml, and config.spec.json).
nginx-relay supports two types of virtual domain:
passthrough: use this for servers in your private network ready to handle HTTP, HTTPS, and certificates- HTTP and SSL traffic is forwarded as-is to the back-end
- optionally the PROXY protocol can be used to transmit the requesting IP
direct-serve: use this to add SSL to your insecure servers in the back-end (e.g. a node or fastapi server)- nginx-relay handles certificate creation and renewal
- with the exception of
/.well-known/acme-challenge/, HTTP traffic is redirected to HTTPS, and HTTPS traffic is proxied to the back-end (if present) - supports client certificates to only allow access to known parties
- supports IP-based trust mechanism to override the need for client certificate
- supports server certificates for the back-end
Setup
Prerequisites
- Docker and Docker Compose
- A domain pointing to this server's IP (for Let's Encrypt)
1. Create the configuration
Copy the example and edit it (JSON or YAML):
cp config.example.json config.json
# or
cp config.example.yaml config.yamlEdit the config file with your domains. The settings block configures ports and the Let's Encrypt email:
{
"settings": {
"http-port": 80,
"https-port": 443,
"internal-https-port": 8943,
"certbot-email": "you@example.com"
}
}Add domains under pass-through (L4 forwarding) or direct-serve (L7 TLS termination):
{
"pass-through": [
{
"domain": "app.example.com",
"address": "192.168.1.10",
"https-port": 443
}
],
"direct-serve": [
{
"domain": "www.example.com",
"address": "192.168.1.20",
"http-port": 8080
}
]
}See config.spec.json for the full schema, and config.example.json or config.example.yaml for complete examples.
2. Production certificates
NB: by default, nginx-relay obtains test certificates via certbot; the reason is that if something's wrong with the configuration, a few attempts to issue a Let's Encrypt certificate are enough to temporarily ban further attempts.
To issue actual Let's Encrypt certificates, set the PRODUCTION environment variable:
For docker, update the corresponding environment variable in the compose file.
3. Build and run
docker compose -f docker-compose-jinja.yml build docker compose -f docker-compose-jinja.yml up -d
4. Verify
# Health check curl http://localhost/health # Check logs docker compose -f docker-compose-jinja.yml logs -f
Client Certificate Authentication (mTLS)
You can require clients to present a certificate signed by a trusted CA. This is configured per direct-serve domain.
1. Create a CA
mkdir -p client-certificates # Generate CA key and self-signed certificate openssl genrsa -out client-certificates/ca.key 4096 openssl req -new -x509 -days 3650 -key client-certificates/ca.key -out client-certificates/ca.pem -subj "/CN=My Client CA"
2. Issue a client certificate
# Generate client key and CSR openssl genrsa -out client.key 4096 openssl req -new -key client.key -out client.csr -subj "/CN=my-client" # Sign with the CA openssl x509 -req -days 3650 -in client.csr -CA client-certificates/ca.pem -CAkey client-certificates/ca.key -CAcreateserial -out client.pem # Clean up rm client.csr
You can issue multiple client certificates from the same CA. They will all be trusted without any config changes.
3. Enable in config
Add client-certificate to a direct-serve entry, listing the CA certificate filenames:
{
"direct-serve": [
{
"domain": "secure.example.com",
"address": "localhost",
"http-port": 8080,
"client-certificate": ["ca.pem"]
}
]
}Multiple CA files can be listed; they are concatenated into a single trust bundle.
The client-certificate-directory setting controls where the files are read from (default: client-certificates/):
{
"settings": {
"client-certificate-directory": "client-certificates/"
}
}4. Restart
docker compose -f docker-compose-jinja.yml up -d --build
5. Create a PKCS12 bundle for browsers
Browsers and some HTTP clients expect a .p12 (PKCS12) file that bundles the client certificate and private key together:
openssl pkcs12 -export -out client.p12 -inkey client.key -in client.pem -certfile client-certificates/ca.pem
You will be prompted to set an export password. Import the resulting client.p12 into your browser or OS certificate store.
Note: nginx itself does not use the .p12 file — it only needs the CA certificate (ca.pem) in PEM format for verification. The PKCS12 bundle is purely for the client side.
6. Test with curl
# With client cert (succeeds) curl --cert client.pem --key client.key https://secure.example.com # Without client cert (rejected by nginx) curl https://secure.example.com
If the server uses staging Let's Encrypt certs, add -k to skip server cert verification.
Skipping Client Certificate for Specific Networks
You can exempt certain IP ranges from client certificate verification using skip-client-certificate-for-networks. This is useful when trusted internal networks should bypass mTLS while external clients are still required to present a certificate.
The option accepts a single CIDR or a list of CIDRs:
{
"direct-serve": [
{
"domain": "secure.example.com",
"address": "localhost",
"http-port": 8080,
"client-certificate": ["ca.pem"],
"skip-client-certificate-for-networks": ["10.0.0.0/8", "192.168.1.0/24"]
}
]
}A single CIDR is also accepted:
"skip-client-certificate-for-networks": "10.0.0.0/8"
Clients connecting from matching IPs will not be asked for a client certificate. All other clients must still present a valid certificate signed by a listed CA.
A warning is printed during configuration compilation if skip-client-certificate-for-networks is set but client-certificate is not configured, since there is nothing to skip.
Server Certificate Verification
You can optionally require the back-end servers for direct-serve to have a certificate released by a trusted certificate authority, just like for client certificates.
Assuming ca.key and ca.pem are the certificate authority details (as above), you can create a SSL certificate as follows:
openssl genrsa -out caddy-cert.key 4096
openssl req -new -key caddy-cert.key -out caddy-cert.csr -subj "/CN=my-client" -addext "subjectAltName=DNS:192.168.178.55,DNS:localhost,DNS:127.0.0.1"
openssl x509 -req -days 3650 -in caddy-cert.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out caddy-cert.pem -copy_extensions copyall
You'll then use ca.pem in the configuration.
E.g.:
...
{
"domain": "trust-me.example.com",
"address": "192.168.5.6",
"http-port": 2283,
"server-certificate": ["ca.pem"],
}
nginx-relay will look for the specified certificates under the directory specified by settings.server-certificate-directory, which defaults to ./server-certificates.
NB: the certificate must match the address specified in the configuration file for verification to work; this is achieved using the options -addtext"subjectAltName=DNS:<address>" and -copy_extensions copyall in the openssl commands above.
PROXY Protocol
PROXY protocol allows proxies to forward information about the requesting IP.
For PROXY protocol to work, it needs to be enabled both on the proxy and the back-end.
In nginx-relay configuration file:
{
"pass-through": [
{
"domain": "secure.example.com",
"address": "localhost",
"http-port": 8080,
"proxy-protocol": true
}
]
}Enabling PROXY protocol in the back-end: