Skip to content
GitHubLinkedIn

LEF Root CA (internal)

LEF uses an internal Root CA to issue and trust TLS certificates for internal-only hostnames (LAN/VPN) and private service-to-service TLS.

Internal CA certificates are not publicly trusted. Use Let’s Encrypt for public internet-facing hostnames.

  • Creating the Root CA (OpenSSL).
  • Issuing leaf certificates (including internal wildcards) with an issuance script.
  • Handling re-issuance when OpenSSL reports “There is already a certificate for /CN=…”.
  • Renewal/expiry workflow and client trust installation.

This is the OpenSSL “CA directory” pattern. Pick a CA directory (example uses $HOME/Certificates/lef-ca).

export CA_DIR="$HOME/Certificates/lef-ca"
mkdir -p "$CA_DIR"/{certs,crl,newcerts,private}
chmod 700 "$CA_DIR/private"
touch "$CA_DIR/index.txt"
echo 1000 > "$CA_DIR/serial"
openssl genrsa -aes256 -out "$CA_DIR/private/ca.key.pem" 4096
chmod 400 "$CA_DIR/private/ca.key.pem"
openssl req -x509 -new -nodes -key "$CA_DIR/private/ca.key.pem" \
  -sha256 -days 3650 \
  -subj "/C=BR/ST=SP/L=Sao Paulo/O=LEF/OU=IT/CN=LEF Root CA" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash" \
  -addext "authorityKeyIdentifier=keyid:always,issuer" \
  -out "$CA_DIR/certs/ca.cert.pem"
chmod 444 "$CA_DIR/certs/ca.cert.pem"

Issue leaf certificates (including internal wildcards)

Section titled “Issue leaf certificates (including internal wildcards)”

This repo includes an example script that:

  • Issues *.cert.pem + *.key.pem using openssl ca
  • Reuses an existing cert when the CA database already has a valid cert for the same CN and the local key matches
  • Supports re-issuing (rotation) when the existing cert is expired or near expiry
sign-cert.sh
#!/usr/bin/env bash
# sign-cert.sh
#
# Issue (or reuse) an internal TLS certificate from the LEF Root CA using OpenSSL.
#
# Usage:
#   ./sign-cert.sh [options] <basename> <commonName> <dns1> [dns2 ...]
#
# Outputs:
#   <basename>.key.pem   (private key)
#   <basename>.cert.pem  (issued certificate; PEM)
#
# Default behavior:
# - If the CA database already contains a valid certificate for the same subject
#   (e.g. `/CN=*.core.lef`) AND the local key matches, reuse it by copying the
#   issued cert from the CA `newcerts/` store into `<basename>.cert.pem`.
# - If the existing cert is expired or expiring soon, re-issue (optionally with
#   a new key).
#
# Notes:
# - NGINX must reference the issued certificate (BEGIN CERTIFICATE), not a CSR
#   (BEGIN CERTIFICATE REQUEST).
# - This script never writes to the CA database unless it is issuing a new cert.

set -euo pipefail

CA_DIR="${CA_DIR:-$HOME/Certificates/lef-ca}"
REISSUE=0
NEW_KEY=0
RENEW_WITHIN_DAYS=30

usage() {
  cat >&2 <<EOF
Usage:
  $0 [--ca-dir <path>] [--renew-within-days <days>] [--reissue] [--new-key] <basename> <commonName> <dns1> [dns2 ...]

Options:
  --ca-dir <path>            Path to CA directory (default: \$CA_DIR or "$HOME/Certificates/lef-ca")
  --renew-within-days <n>    Re-issue if existing cert expires within N days (default: $RENEW_WITHIN_DAYS)
  --reissue                  Force re-issue even if a valid cert exists for the same subject
  --new-key                  Generate a new key before issuing (rotation)

Examples:
  $0 core-lef-wildcard "*.core.lef" "*.core.lef"
  $0 docs-lef-wildcard "docs.lef" "docs.lef" "*.docs.lef"
  $0 --renew-within-days 30 --reissue --new-key core-lef-wildcard "*.core.lef" "*.core.lef"
EOF
  exit 2
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --ca-dir) CA_DIR="$2"; shift 2 ;;
    --renew-within-days) RENEW_WITHIN_DAYS="$2"; shift 2 ;;
    --reissue) REISSUE=1; shift ;;
    --new-key) NEW_KEY=1; shift ;;
    -h|--help) usage ;;
    --) shift; break ;;
    -*) echo "Unknown option: $1" >&2; usage ;;
    *) break ;;
  esac
done

[[ $# -ge 3 ]] || usage

BASENAME="$1"
CN="$2"
shift 2

INDEX_FILE="$CA_DIR/index.txt"
NEWCERTS_DIR="$CA_DIR/newcerts"
SERIAL_FILE="$CA_DIR/serial"
CA_KEY="$CA_DIR/private/ca.key.pem"
CA_CERT="$CA_DIR/certs/ca.cert.pem"

[[ -f "$INDEX_FILE" ]] || { echo "CA index not found: $INDEX_FILE" >&2; exit 1; }
[[ -d "$NEWCERTS_DIR" ]] || { echo "CA newcerts dir not found: $NEWCERTS_DIR" >&2; exit 1; }
[[ -f "$SERIAL_FILE" ]] || { echo "CA serial file not found: $SERIAL_FILE" >&2; exit 1; }
[[ -f "$CA_KEY" ]] || { echo "CA private key not found: $CA_KEY" >&2; exit 1; }
[[ -f "$CA_CERT" ]] || { echo "CA cert not found: $CA_CERT" >&2; exit 1; }

subject="/CN=${CN}"
renew_seconds="$((RENEW_WITHIN_DAYS * 86400))"

find_latest_valid_serial() {
  # index.txt format (tab-separated):
  #   V  YYMMDDHHMMSSZ  <revocation>  <serial>  <filename>  <subject>
  awk -v subj="$subject" '$1=="V" && $NF==subj {print $4}' "$INDEX_FILE" | tail -n 1
}

find_cert_path_by_serial() {
  local serial="$1"
  local default_path="$NEWCERTS_DIR/${serial}.pem"
  if [[ -f "$default_path" ]]; then
    echo "$default_path"
    return 0
  fi

  # Fallback: some OpenSSL configs use different extensions.
  find "$NEWCERTS_DIR" -maxdepth 1 -type f -name "${serial}.*" -print | head -n 1 || true
}

key_matches_cert() {
  local key="$1"
  local cert="$2"
  local cert_mod key_mod
  cert_mod="$(openssl x509 -noout -modulus -in "$cert" | openssl sha256)"
  key_mod="$(openssl rsa  -noout -modulus -in "$key"  | openssl sha256)"
  [[ "$cert_mod" == "$key_mod" ]]
}

if [[ $REISSUE -eq 0 ]]; then
  existing_serial="$(find_latest_valid_serial || true)"
  if [[ -n "${existing_serial:-}" ]]; then
    existing_cert="$(find_cert_path_by_serial "$existing_serial")"
    if [[ -z "${existing_cert:-}" || ! -f "$existing_cert" ]]; then
      echo "Found existing serial $existing_serial in CA DB, but cert file not found in $NEWCERTS_DIR" >&2
      exit 1
    fi

    # If the cert will expire within the renew window, treat it as needing reissue.
    if ! openssl x509 -checkend "$renew_seconds" -noout -in "$existing_cert" >/dev/null 2>&1; then
      REISSUE=1
    else
      if [[ -f "${BASENAME}.key.pem" ]] && key_matches_cert "${BASENAME}.key.pem" "$existing_cert"; then
        cp "$existing_cert" "${BASENAME}.cert.pem"
        echo "✅ Reused existing cert (serial $existing_serial) -> ${BASENAME}.cert.pem"
        exit 0
      fi

      echo "Existing cert already issued for CN=$CN, but local key is missing or does not match." >&2
      echo "- Restore the original matching ${BASENAME}.key.pem to reuse it, OR" >&2
      echo "- Re-run with --reissue (and optionally --new-key) to rotate." >&2
      exit 3
    fi
  fi
fi

# Build SAN extension file.
tmp_ext="$(mktemp)"
trap 'rm -f "$tmp_ext"' EXIT

ALT_NAMES=""
idx=1
for dns in "$@"; do
  ALT_NAMES+="DNS.${idx} = ${dns}\n"
  idx=$((idx + 1))
done

printf "[ ext ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[ alt_names ]
${ALT_NAMES}" >"$tmp_ext"

# Key handling.
if [[ $NEW_KEY -eq 1 || ! -f "${BASENAME}.key.pem" ]]; then
  openssl genrsa -out "${BASENAME}.key.pem" 2048
fi

# CSR (kept on failure for debugging; removed on success).
CSR_FILE="${BASENAME}.csr.pem"
openssl req -new -key "${BASENAME}.key.pem" -subj "/CN=${CN}" -out "$CSR_FILE"

UNIQUE_SUBJECT="yes"
if [[ $REISSUE -eq 1 ]]; then
  UNIQUE_SUBJECT="no"
fi

openssl ca -batch -config <(
  cat <<EOF
[ ca ]
default_ca = CA_default

[ CA_default ]
dir = $CA_DIR
certs = \$dir/certs
crl_dir = \$dir/crl
new_certs_dir = \$dir/newcerts
database = \$dir/index.txt
serial = \$dir/serial
private_key = \$dir/private/ca.key.pem
certificate = \$dir/certs/ca.cert.pem
default_days = 825
default_md = sha256
policy = policy_loose
unique_subject = $UNIQUE_SUBJECT

[ policy_loose ]
commonName = supplied
EOF
) \
  -extensions ext -extfile "$tmp_ext" \
  -out "${BASENAME}.cert.pem" -infiles "$CSR_FILE"

rm -f "$CSR_FILE"

echo "✅ Issued ${BASENAME}.cert.pem"

Issue a wildcard cert (for *.core.lef):

./sign-cert.sh core-lef-wildcard "*.core.lef" "*.core.lef"

Issue a cert that covers both the zone apex and wildcard (example docs.lef + *.docs.lef):

./sign-cert.sh docs-lef-wildcard "docs.lef" "docs.lef" "*.docs.lef"

Why “There is already a certificate for /CN=…” happens (and what to do)

Section titled “Why “There is already a certificate for /CN=…” happens (and what to do)”

openssl ca enforces “one valid cert per subject” by default (unique_subject = yes). If a valid certificate already exists in the CA database for the same subject (e.g. /CN=*.core.lef), OpenSSL refuses to sign another one.

You have two safe choices:

  1. Reuse the existing certificate (no rotation): the script will copy the already-issued cert from the CA database into <basename>.cert.pem (only if the local key matches).
  2. Re-issue / rotate (new certificate, optionally new key): run the script with --reissue (and optionally --new-key). This allows duplicate subjects for that issuance.

Renewal / expiry workflow (internal NGINX certs)

Section titled “Renewal / expiry workflow (internal NGINX certs)”
  1. Check expiry:
openssl x509 -in <file>.cert.pem -noout -subject -issuer -enddate
  1. Re-issue before expiry (example renew within 30 days, rotate key):
./sign-cert.sh --renew-within-days 30 --reissue --new-key core-lef-wildcard "*.core.lef" "*.core.lef"
  1. Install on the target server (example for NGINX):
sudo install -m 0644 core-lef-wildcard.cert.pem /etc/nginx/ssl/core-lef-wildcard.cert.pem
sudo install -m 0600 core-lef-wildcard.key.pem  /etc/nginx/ssl/core-lef-wildcard.key.pem
sudo nginx -t && sudo systemctl reload nginx

For Windows servers, LEF uses Active Directory Certificate Services (AD CS) for certificate enrollment and trust.

This typically simplifies Windows trust distribution (domain-joined clients can trust the AD CS chain), and avoids exporting private keys unnecessarily.

See:

Linux (system trust store):

sudo cp <ca-root>.crt /usr/local/share/ca-certificates/lef-internal.crt
sudo update-ca-certificates

Windows:

  1. Run mmc.exe → Add Certificates snap-in (Local Computer)
  2. Import the CA root certificate into Trusted Root Certification Authorities
  • Installing multiple internal “root CAs” creates trust fragmentation; prefer a single internal CA.
  • Private keys must never be committed to Git or copied into shared locations without encryption.
  • If a cert is re-issued, clients that don’t trust the Root CA (or that pin the old cert) will see TLS errors until trust is fixed.