Building a Proper PKI Chain of Trust in a Samba AD Lab

2025-05-205 min read

Overview

A flat, single-tier CA works until it doesn't -- and in an enterprise environment, it never would. In this post, I restructured my lab's certificate infrastructure into a proper two-tier PKI: an offline Root CA that signs an Intermediate CA running on my Samba DC. This mirrors how production environments separate CA responsibilities for security and manageability, and it gave me a foundation for issuing trusted service certificates to Traefik, Authentik, and LDAPS.


Why a PKI Chain of Trust?

In enterprise networks, it's a best practice to:

  • Keep the Root Certificate Authority (CA) offline to minimize risk
  • Delegate signing authority to an Intermediate CA
  • Sign service certificates (LDAPS, HTTPS, etc.) from the Intermediate

Initially, my Samba AD DC handled both Root CA and service cert issuance, which is not secure long-term. This project restructures that into a hardened, tiered architecture.


PKI Topology

Root CA (Offline)
 └── Intermediate CA (Samba DC)
     ├── samba-ldaps.crt
     ├── authentik.tld
     └── traefik.tld

Phase 1: Offline Root CA

I did this on a separate offline VM.

hljs bash
mkdir -p ~/rootCA/{certs,crl,newcerts,private,csr}
touch ~/rootCA/index.txt
echo 1000 > ~/rootCA/serial
chmod 700 ~/rootCA/private

Generate the Root CA key and certificate:

hljs bash
openssl genrsa -aes256 -out ~/rootCA/private/rootCA.key.pem 4096
openssl req -x509 -new -key ~/rootCA/private/rootCA.key.pem \
  -sha256 -days 3650 -out ~/rootCA/certs/rootCA.cert.pem \
  -subj "/C=US/ST=State/O=Lab/OU=PKI/CN=Root CA"

Phase 2: Intermediate CA (Samba DC)

On the Samba DC, generate the Intermediate CA private key and CSR:

hljs bash
mkdir -p /usr/local/samba/private/pki/intermediate
cd /usr/local/samba/private/pki/intermediate
openssl genrsa -out intermediate.key.pem 4096
openssl req -new -key intermediate.key.pem \
  -out intermediate.csr.pem \
  -subj "/C=US/ST=State/O=Lab/OU=CA/CN=Intermediate CA"

Transfer the CSR to the offline Root CA host, then sign it:

hljs bash
openssl ca -config openssl_root.cnf \
  -extensions v3_intermediate_ca \
  -days 1825 -notext -md sha256 \
  -in csr/intermediate.csr.pem \
  -out certs/intermediate.cert.pem

Create the chain file:

hljs bash
cat certs/intermediate.cert.pem certs/rootCA.cert.pem > certs/ca-chain.cert.pem

Transfer intermediate.cert.pem and ca-chain.cert.pem back to the Samba server.


Samba TLS Configuration

Place the following files on the Samba DC:

/usr/local/samba/private/tls/
├── intermediate.key
├── intermediate.crt
├── ca-chain.crt

Edit smb.conf:

[global]
tls enabled  = yes
tls keyfile  = /usr/local/samba/private/tls/intermediate.key
tls certfile = /usr/local/samba/private/tls/intermediate.crt
tls cafile   = /usr/local/samba/private/tls/ca-chain.crt

Restart the Samba service:

hljs bash
systemctl restart samba-ad-dc

Verify LDAPS is working:

hljs bash
openssl s_client -connect samba.domain.lan:636 -showcerts

Service Certificate Automation

To issue certs for services like Traefik and Authentik, I wrote a bash script:

/usr/local/samba/private/tls/sign_service_cert.sh

Script Highlights

hljs bash
#!/bin/bash
FQDN="$1"
CERT_DIR="/usr/local/samba/private/tls/$FQDN"
mkdir -p "$CERT_DIR"

openssl req -new -nodes -newkey rsa:2048 \
  -keyout "$CERT_DIR/$FQDN.key" \
  -out "$CERT_DIR/$FQDN.csr" \
  -subj "/CN=$FQDN" \
  -config <(cat <<EOF
[ req ]
default_bits       = 2048
prompt             = no
default_md         = sha256
req_extensions     = v3_req
distinguished_name = dn

[ dn ]
CN = $FQDN

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = $FQDN
EOF
)

openssl x509 -req \
  -in "$CERT_DIR/$FQDN.csr" \
  -CA /usr/local/samba/private/tls/intermediate.crt \
  -CAkey /usr/local/samba/private/tls/intermediate.key \
  -CAcreateserial \
  -out "$CERT_DIR/$FQDN.crt" \
  -days 825 -sha256 \
  -extfile <(cat <<EOF
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = $FQDN
EOF
)

cat "$CERT_DIR/$FQDN.crt" /usr/local/samba/private/tls/ca-chain.crt > "$CERT_DIR/$FQDN-fullchain.crt"

Usage:

./sign_service_cert.sh authentik.domain.lan

Traefik + Authentik Certificate Integration

Problem

The Authentik outpost container failed to connect with:

tls: failed to verify certificate: x509: certificate signed by unknown authority

Solution: Dockerfile CA Injection

I created a Dockerfile to embed the Root CA into the outpost container:

FROM ghcr.io/goauthentik/proxy:latest

USER root
COPY ./certs/rootCA.crt /usr/local/share/ca-certificates/rootCA.crt
RUN apt update && apt install -y ca-certificates && update-ca-certificates
USER 1000

Build the container:

hljs bash
docker compose build --no-cache authentik-outpost

Verify trust inside the running container:

hljs bash
docker exec -it traefik-authentik-outpost bash
openssl s_client -connect authentik.domain.lan:443 -CAfile /etc/ssl/certs/ca-certificates.crt

You should see:

Verify return code: 0 (ok)

Active Directory GPO

To deploy trust across my Windows clients, I used Group Policy to distribute the Root CA certificate only -- not the entire chain. This prevents clients from implicitly trusting intermediate certificates not issued by the Root.

To deploy:

  • Open Group Policy Management Console
  • Create or edit a GPO
  • Navigate to: Computer Configuration > Policies > Windows Settings > Security Settings > Public Key Policies > Trusted Root Certification Authorities
  • Import rootCA.cert.pem

After syncing (gpupdate /force), all domain-joined clients trust the Root CA.


Lessons Learned

  • Keep the Root CA offline -- no exceptions. If the Root key is compromised, your entire trust chain is toast. An air-gapped VM or USB-booted system works fine for a lab.
  • The chain file order matters. cat intermediate.crt rootCA.crt > ca-chain.crt -- intermediate first, then root. Get it backwards and clients will reject the chain.
  • Inject the Root CA, not the chain, into containers. Docker containers only need the Root CA in their trust store. The server presents the chain; the client validates it against the root.
  • Distribute only the Root CA via GPO. Pushing the full chain to Windows clients can cause unexpected trust issues. Let the server present the intermediate; let the client verify up to the root.
  • Automate cert issuance early. The signing script saved me significant time once I started issuing certs for Traefik, Authentik, and LDAPS. Manual cert generation does not scale.

Summary

This project resulted in a secure, enterprise-style certificate infrastructure for my home lab:

  • Hardened Root CA hosted offline
  • Samba promoted to Intermediate CA
  • Signed certificates for LDAPS, Traefik, and Authentik
  • Trusted CA distributed via AD GPO
  • Docker containers extended to trust my internal CA

This foundation supports advanced secure services like LDAPS, OIDC, and internal HTTPS endpoints with verified trust.