I wanted a single-user, private HTTPS site that only lets me in—no passwords, no TOTP, just my Romanian eID (CEI) card. The plan: use the card’s client certificate for mTLS and have Nginx only serve content when it sees my cert.

This post is a practical, copy-paste log of what I did, the exact errors I hit, and how I fixed them. It’s written in the same “get to the point + show the commands” style I used in my Reverse SSH Tunnel Tutorial — just applied to PKI this time.

What’s on the Romanian eID?

On Linux, listing the token with pkcs11-tool -O showed: My Authentication certificate (ECC P-384). The issuing RO CEI MAI Sub-CA. The RO CEI MAI Root-CA.

That’s enough to build the trust chain locally even if public CA URLs are flaky. (Paper note: we’ll export these from the card and feed them to Nginx.)

Exporting the Sub-CA and Root-CA

You can export by ID or by pkcs11: URI (I had fewer surprises with p11tool):

PROV=/usr/lib/idplugclassic/libidplug-pkcs11.so
p11tool --provider="$PROV" --list-all   # discover URIs

# If you only have the hex ID, percent-encode it:
hex_id_sub=3f6b16cd16e71232dc0cfa1f3c600ca453d47fe9
pct_id_sub=$(echo "$hex_id_sub" | sed 's/../%&/g')

# Export Sub-CA and Root-CA (DER)
p11tool --provider="$PROV" \
  --export "pkcs11:model=ID-A;manufacturer=DGEP;token=PKI%20Application%20%28User%20PIN%29;id=$pct_id_sub;type=cert" \
  > ro_cei_mai_sub-ca.der

hex_id_root=e63c8507b35553a635e37da34d98e592d6b4ceed
pct_id_root=$(echo "$hex_id_root" | sed 's/../%&/g')

p11tool --provider="$PROV" \
  --export "pkcs11:model=ID-A;manufacturer=DGEP;token=PKI%20Application%20%28User%20PIN%29;id=$pct_id_root;type=cert" \
  > ro_cei_mai_root-ca.der

# Convert to PEM and bundle
openssl x509 -inform der -in ro_cei_mai_sub-ca.der  -out ro_cei_mai_sub-ca.pem
openssl x509 -inform der -in ro_cei_mai_root-ca.der -out ro_cei_mai_root-ca.pem
cat ro_cei_mai_sub-ca.pem ro_cei_mai_root-ca.pem > cei-mai-ca-bundle.pem

I placed the bundle here: /home/user/mtls-cei/cei/cei-mai-ca-bundle.pem

Self-signed server TLS

I didn’t want Let’s Encrypt/port 80. So I generated a self-signed server cert:

mkdir -p /srv/mtls/server
openssl ecparam -genkey -name prime256v1 -out /srv/mtls/server/server.key
openssl req -new -x509 -days 1825 -key /srv/mtls/server/server.key \
  -out /srv/mtls/server/server.crt -subj "/CN=mtls.local"

Dockerized Nginx for mTLS

Folder layout on the host:

/home/user/mtls-cei/
  mtls.conf
  index.html
  cei/
    server.crt
    server.key
    cei-mai-ca-bundle.pem

docker-compose.yml service:

services:
  mtls-cei:
    image: nginx:latest
    container_name: mtls-cei
    restart: always
    networks: [ main_net ]
    ports:
      - "9033:443/tcp"    # external 9033 -> container 443
    volumes:
      - "/home/user/mtls-cei/:/usr/share/nginx/html:ro"
      - "/home/user/mtls-cei/mtls.conf:/etc/nginx/conf.d/mtls.conf:ro"
      - "/home/user/mtls-cei/cei:/etc/nginx/cei:ro"

/home/user/mtls-cei/mtls.conf:

server {
    listen 443 ssl;
    server_name _;

    ssl_certificate     /etc/nginx/cei/server.crt;
    ssl_certificate_key /etc/nginx/cei/server.key;

    # trust the issuing Sub-CA + Root-CA (from the card)
    ssl_client_certificate /etc/nginx/cei/cei-mai-ca-bundle.pem;

    # important while testing: request the cert but don't hard-fail yet
    ssl_verify_client optional;
    ssl_verify_depth 3;

    # block requests that didn't present a valid client cert
    if ($ssl_client_verify != SUCCESS) { return 403; }

    # allow only my card (Subject DN serialNumber from my eID)
    if ($ssl_client_s_dn !~ "serialNumber=0101010101010101") { return 403; }

    # debug endpoint: prints what the server saw
    location = /whoami {
        add_header Content-Type text/plain;
        return 200 "DN: $ssl_client_s_dn\nFP: $ssl_client_fingerprint\nVerify: $ssl_client_verify\n";
    }

    location / {
        root /usr/share/nginx/html;
        index index.html;
    }
}

Bring the service up:

docker compose up -d --force-recreate mtls-cei
docker logs -f mtls-cei

Now I can visit: https://<server-ip>:9033/ and my browser should ask for a client certificate.