Putting EJBCA behind a reverse proxy with a trusted TLS certificate

So normally you'd deploy EJBCA internally. But what if you wanted to publicly use it for SCEP but not want to deploy the ManagementCA to user trust store?

That's when you'd put EJBCA behind a reverse proxy, but how?

Prerequisites

Prepare your own root CA: You'll need the keypair as a PKCS#12 file and the root CA certificate itself as a PEM file.

Put the root CA certificate to somewhere the reverse proxy can access. /opt/root_ca.pem would be an example. Make sure it is readable by everyone: chmod 644 /opt/root_ca.pem

DNS

Make sure you've set up a (sub-)domain that points to your EJBCA server. For this blog post, I've chosen ejbca.example.com.

Procedure

We'll go over bootstrapping EJBCA itself and the reverse proxy configuration.

EJBCA

Bring up EJBCA using Keyfactor's own Docker Compose file with little modifications:

---
networks:
  access-bridge:
    driver: bridge
  application-bridge:
    driver: bridge
services:
  ejbca-database:
    container_name: ejbca-database
    image: "library/mariadb:latest"
    restart: unless-stopped
    networks:
      - application-bridge
    environment:
      - MYSQL_ROOT_PASSWORD=foo123
      - MYSQL_DATABASE=ejbca
      - MYSQL_USER=ejbca
      - MYSQL_PASSWORD=ejbca
    volumes:
      - ./datadbdir:/var/lib/mysql:rw
  ejbca:
    hostname: ejbca-node1
    container_name: ejbca
    image: keyfactor/ejbca-ce:latest
    depends_on:
      - ejbca-database
    restart: unless-stopped
    networks:
      - access-bridge
      - application-bridge
    environment:
      - DATABASE_JDBC_URL=jdbc:mariadb://ejbca-database:3306/ejbca?characterEncoding=UTF-8
      - LOG_LEVEL_APP=INFO
      - LOG_LEVEL_SERVER=INFO
      - TLS_SETUP_ENABLED=later
      - PROXY_HTTP_BIND=0.0.0.0
    ports:
      # Proxy port for the HTTP endpoint (normally 8080 when PROXY_HTTP_BIND is not set)
      - "127.0.0.1:8081:8081"
      # Proxy port for the HTTPS endpoint, it is HTTP but has
      # SSL_CLIENT_CERT header support to provide the
      # equivalent of the regular HTTPS endpoint
      # (normally 8443 when PROXY_HTTP_BIND is not set)
      - "127.0.0.1:8082:8082"

Aside from the addition of restart: unless-stopped, notice that the environment variables TLS_SETUP_ENABLED is set to later and PROXY_HTTP_BIND is set to 0.0.0.0. These two makes few changes:

Reverse Proxy

I chose NGINX for this, but you can choose any reverse proxy you want that supports mTLS with a different CA other than the one used for the web service.

Make sure you don't have any firewall rules blocking the ports 80 and 443.

Since I am doing all this in Enterprise Linux, the config file structure is a bit different than the Debian packaging; so be wary.

After install, make a copy of /etc/nginx/nginx.conf to a temporary place, you're going to use it later on: cp -a /etc/nginx/nginx.conf /tmp/nginx_prebootstrap.conf

Bootstrapping TLS with certbot

Set the server_name variable in the /etc/nginx/nginx.conf file to the (sub-)domain you've chosen.

Make sure to have the EPEL repository installed and enabled. Install certbot and its NGINX plugin (dnf install certbot python3-certbot-nginx).

Register your ACME account: certbot register --email your_email@example.com --no-eff-email --agree-tos

Obtain your TLS certificate and install it into NGINX automatically: certbot --nginx --domains ejbca.example.com --key-type rsa

Re-configuring NGINX

Copy back the config file from earlier: cp -af /tmp/nginx_prebootstrap.conf /etc/nginx/nginx.conf

Comment out the default server block that's below the line include /etc/nginx/conf.d/*.conf; in /etc/nginx/nginx.conf. It should look like this:

A terminal screenshot showing the nano editor with the NGINX configuration being open.

We're going to create two files: /etc/nginx/default.d/proxy.conf and /etc/nginx/conf.d/ejbca.conf.

/etc/nginx/default.d/proxy.conf:

proxy_set_header Host              $host;
proxy_set_header X-Real-IP         $remote_addr;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host  $host;
proxy_set_header Connection        "";

The following virtual server configuration is somewhat translated from what Keyfactor documentation provides with Apache2, with a slight tweak of using regular HTTP instead of using AJP.

/etc/nginx/conf.d/ejbca.conf:

server {
    listen       80;
    listen       [::]:80;
    server_name  ejbca.example.com;
    root         /usr/share/nginx/html;

    access_log /var/log/nginx/ejbca_http_access.log combined;
    error_log  /var/log/nginx/ejbca_http_error.log  warn;

    include /etc/nginx/default.d/proxy.conf;

    # CRL Distribution Point
    location = /publicweb/webdist/certdist {
        if ($arg_cmd = "crl") {
            rewrite ^(.*)$ /ejbca$1 break;
            proxy_pass http://127.0.0.1:8081;
            break;
        }
        return 301 https://$host$request_uri;
    }

    # Healthcheck and OCSP
    location /publicweb/status/ {
        rewrite ^(.*)$ /ejbca$1 break;
        proxy_pass http://127.0.0.1:8081;
    }

    location /ejbca/ {
        # Only the CRL/OCSP/healthcheck endpoints are allowed in the clear;
        # any other request redirect to HTTPS.
        if ($request_uri !~ ^/ejbca/(publicweb/webdist/certdist\?.*cmd=crl|publicweb/status/)) {
            return 301 https://$host$request_uri;
        }
        proxy_pass http://127.0.0.1:8081;
    }

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

server {
    listen 443 ssl; # managed by Certbot
    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    http2 on;
    server_name  ejbca.example.com;

    root         /usr/share/nginx/html;

    access_log /var/log/nginx/ejbca_https_access.log combined;
    error_log  /var/log/nginx/ejbca_https_error.log  warn;

    ssl_certificate /etc/letsencrypt/live/ejbca.example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/ejbca.example.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    ssl_client_certificate    /opt/root_ca.pem;
    ssl_verify_client         optional;
    ssl_verify_depth          1;

    location /ejbca/adminweb {
        #if ($ssl_client_verify != SUCCESS) { return 403; } # uncomment after creating and installing your SuperAdmin certificate then remove the SuperAdmin public access policy.
        proxy_pass http://127.0.0.1:8082;
        include /etc/nginx/default.d/proxy.conf;
        proxy_set_header SSL_CLIENT_CERT $ssl_client_cert;
    }
    location /adminweb {
        #if ($ssl_client_verify != SUCCESS) { return 403; } # uncomment after creating and installing your SuperAdmin certificate then remove the SuperAdmin public access policy.
        rewrite ^(.*)$ /ejbca$1 break;
        proxy_pass http://127.0.0.1:8082;
        include /etc/nginx/default.d/proxy.conf;
        proxy_set_header SSL_CLIENT_CERT $ssl_client_cert;
    }


    location /ejbca/ {
        proxy_pass http://127.0.0.1:8082;
        include /etc/nginx/default.d/proxy.conf;
        proxy_set_header SSL_CLIENT_CERT $ssl_client_cert;
    }
    location / {
        rewrite ^(.*)$ /ejbca$1 break;
        proxy_pass http://127.0.0.1:8082;
        include /etc/nginx/default.d/proxy.conf;
        proxy_set_header SSL_CLIENT_CERT $ssl_client_cert;
    }
}

Make sure to have these files readable by NGINX: chmod 644 /etc/nginx/default.d/proxy.conf /etc/nginx/conf.d/ejbca.conf

To make sure everything went okay, reboot the entire server.



Copyright 2018-2026, linuxgemini (İlteriş Yağıztegin Eroğlu). Any and all opinions listed here are my own and not representative of my employers; future, past and present.