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.
What this covers
Section titled “What this covers”- 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.
Create the Root CA (how it was made)
Section titled “Create the Root CA (how it was made)”This is the OpenSSL “CA directory” pattern. Pick a CA directory (example uses $HOME/Certificates/lef-ca).
1) Directory structure
Section titled “1) Directory structure”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"2) Root private key
Section titled “2) Root private key”openssl genrsa -aes256 -out "$CA_DIR/private/ca.key.pem" 4096
chmod 400 "$CA_DIR/private/ca.key.pem"3) Self-signed Root CA certificate
Section titled “3) Self-signed Root CA certificate”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.pemusingopenssl ca - Reuses an existing cert when the CA database already has a valid cert for the same
CNand the local key matches - Supports re-issuing (rotation) when the existing cert is expired or near expiry
#!/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"Usage examples
Section titled “Usage examples”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:
- 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). - 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)”- Check expiry:
openssl x509 -in <file>.cert.pem -noout -subject -issuer -enddate- 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"- 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 nginxWindows Server certificates (AD CS)
Section titled “Windows Server certificates (AD CS)”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:
Trust the Root CA on clients
Section titled “Trust the Root CA on clients”Linux (system trust store):
sudo cp <ca-root>.crt /usr/local/share/ca-certificates/lef-internal.crt
sudo update-ca-certificatesWindows:
- Run
mmc.exe→ AddCertificatessnap-in (Local Computer) - Import the CA root certificate into
Trusted Root Certification Authorities
Known risks / failure modes
Section titled “Known risks / failure modes”- 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.