
My homelab has three domain controllers, two clients, three iLO management boards and, until last week, exactly zero certificates anyone had a reason to trust. Every RDP session started with a warning, every iLO greeted me with “Your connection is not private” (issuer: Default Issuer (Do not trust), which is at least honest), and the domain controllers didn’t speak LDAPS at all.
I don’t run many internal web services, so for a while I assumed a private PKI was a solution looking for a problem. Wrong. A Windows domain has plenty of places where certificates quietly make things better. The trick is to let Active Directory hand them out on its own, so that I never have to think about renewing them again.
Same workflow as in the previous Windows posts: I decide, and my AI co-admin (Claude, driving PowerShell over WinRM and the iLOs over Redfish) builds it and verifies every step. This time the verification included checking our own TLS handshakes from a Linux box with openssl s_client.
🎯 What actually benefits from certificates
- Domain controllers: with a “Kerberos Authentication” certificate they speak LDAPS on port 636 and can do certificate-based Kerberos (a prerequisite for things like Windows Hello for Business).
- Remote Desktop: RDP uses a self-signed certificate per machine. With a CA-issued one, the “identity of the remote computer cannot be verified” dialog is gone, and a real man-in-the-middle warning would mean something again.
- WinRM over HTTPS (5986): my automation ran over HTTP with NTLM message encryption. It works, but real TLS with a verifiable server identity is better.
- iLO web interfaces: the classic “click through the warning” pages.
- Code signing: a certificate for me, so that my PowerShell startup scripts can be signed and eventually only signed scripts are allowed to run.
Not on the list: the public stuff. Nextcloud and the blog stay with Let’s Encrypt, because a private root is only trusted by my own devices, and that’s the point of it.
🏛️ Design: one tier, on a DC, and why that’s a compromise
The textbook PKI has two tiers: an offline root CA that is switched off in a drawer and only signs the issuing CA, plus an issuing CA that does the daily work. If the issuing CA is compromised, you revoke it and the root survives.
I built a single-tier Enterprise Root CA: “Muench Homelab Root CA”, RSA 4096, SHA-256, valid for 10 years, on HP3. That is a conscious compromise. All three of my servers are domain controllers, and a CA on a DC has real downsides: the server can no longer be renamed, and demoting it gets complicated. Without a member server and without a hypervisor there’s no better home for it right now. When a hypervisor arrives, a two-tier setup with an offline root VM is the natural next step.
Three decisions before the first click:
CAPolicy.infwithLoadDefaultTemplates=0. Without it, a fresh Enterprise CA immediately publishes a dozen default templates with generous enrollment rights. Those defaults are where most of the known AD CS attack paths live. My CA started with zero published templates.- Revocation lists and CA certificate via LDAP only. The default CDP/AIA extensions also point at
http://<ca>/CertEnroll/, a web server that doesn’t exist here. Clients would try it and time out. Every client in this domain can read LDAP, so HTTP is gone. - No web enrollment. The old
/certsrvpages are the target of the NTLM relay attack known as ESC8. Not installed, not needed.
Plus the boring hygiene: CA auditing on, issued certificates capped at two years. And the CA database is part of HP3’s nightly system state backup, which exists since the previous post.
📜 Templates: small, tight and explicit
A certificate template decides what a certificate can do and who may request it. Getting the “who” wrong is how domains get owned: the famous ESC1 is a template where a low-privileged user may put any name, including a domain admin’s, into the request. So there are four templates, each with the narrowest rights that still work:
| Template | Purpose | Subject | Who may enroll |
|---|---|---|---|
| Kerberos Authentication (built-in) | DC certificates: LDAPS, Kerberos | from AD | Domain Controllers (auto) |
| HomelabComputer | RDP, WinRM HTTPS | from AD (CN + DNS SAN = FQDN) | Domain Computers + DCs (auto) |
| HomelabWebServer | iLO, later Windows Admin Center | supplied in the request | Domain Admins only, manual |
| HomelabCodeSigning | signing PowerShell scripts | from AD | me (auto, at logon) |
The WebServer template is the dangerous one, because the requester chooses the name. That is exactly why only Domain Admins may use it. And the CA flag EDITF_ATTRIBUTESUBJECTALTNAME2 (ESC6, “let requesters add any SAN as a request attribute”) stays off. The names come from the CSR itself, which is enough for the iLOs.
Creating templates without a GUI (and three gotchas)
PowerShell has no New-CATemplate. The MMC’s “Duplicate Template” is really just copying an AD object: a pKICertificateTemplate under CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration, plus a matching msPKI-Enterprise-Oid object with a fresh OID below the forest’s OID base. So that’s what we did, attribute by attribute: schema version 2, name flags, enrollment flags, EKUs, then the ACL with the Enroll and AutoEnroll extended rights. Along the way:
- PowerShell variables are case-insensitive.
$Eheld the Enroll GUID,$ewas the loop variable. Guess which one won. - PowerShell unrolls single-element arrays.
@(@('512','manual'))is not a list containing one pair, it’s the pair itself, so the loop happily tried to resolve the user “5”. The fix is the unary comma:@(,@('512','manual')). Add-CATemplateinsisted the brand-new templates “don’t exist in the domain”, whilecertutil -templatelisted them just fine.certutil -SetCATemplates +HomelabComputer,…published them on the first try.
And the evergreen: WinRM ships scripts as Base64 UTF-16 on a command line capped at 8191 characters. One template script grew from 2,970 to 3,170 characters and silently stopped doing anything at all. No error, no output. My co-admin now counts characters before sending. So do I.
🤖 Auto-enrollment: the part where AD does the work
One GPO, homelab – PKI Auto-Enrollment, linked at the domain root: auto-enrollment for computers and users, with “renew expired certificates” and “update pending certificates” enabled. After a gpupdate and certutil -pulse, every DC held two new certificates within a minute: Kerberos Authentication and HomelabComputer, with the correct FQDN, valid for a year. Renewal happens six weeks before expiry, and nobody has to remember anything.
The DCs picked up the Kerberos certificate for LDAPS automatically, without any configuration: the moment a suitable certificate is in the machine store, port 636 answers.
RDP: the policy that did nothing
There is a Group Policy setting made for exactly this: Server authentication certificate template. RDP is supposed to enroll a certificate from that template and use it. It was set, verified in the registry, SessionEnv restarted, TermService restarted on the servers without sessions, and RDP kept presenting its self-signed certificate. There was not a single event in any log explaining why.
Instead of debugging an invisible mechanism, we took the deterministic route: a small startup script in the same GPO. At every boot it picks the newest valid HomelabComputer certificate and binds it to RDP (Win32_TSGeneralSetting.SSLCertificateSHA1Hash). The same script creates or updates the WinRM HTTPS listener with that certificate, and the GPO adds a firewall rule for 5986, LAN only. Machines reboot monthly for patches, and renewal starts six weeks early, so a renewed certificate is always bound in time. The script logs to %ProgramData%\homelab\pki-bind.log, because “it probably ran” is not a status.
🔌 iLO certificates via Redfish
iLOs aren’t domain members, so no auto-enrollment. But iLO 5 can create its own key and CSR, and all of it is scriptable over Redfish:
POST …/SecurityService/HttpsCert/Actions/HpeHttpsCert.GenerateCSRwith the FQDN as common name andIncludeIP: true. Ten to thirty seconds later the CSR shows up inCertificateSigningRequest. It contains a 2048-bit key and SANs for the DNS name and the IP address. The private key never leaves the iLO.- On the CA:
certreq -submit -attrib "CertificateTemplate:HomelabWebServer". …/HpeHttpsCert.ImportCertificatewith the PEM. The iLO resets itself; the host doesn’t notice.
Goodbye, Default Issuer (Do not trust). Since the certificate carries both the name and the IP, the iLO is trusted either way. That matters when DNS is the thing you’re trying to fix.
✅ Trust, but verify (with OpenSSL)
Domain members trust the new root automatically: an Enterprise CA publishes its certificate into AD, and Group Policy distributes it. For the proof I exported the root to the Linux backup box and checked every endpoint the way a strict client would, with name checking:
openssl s_client -connect 192.168.10.30:636 -CAfile muench-homelab-root-ca.crt \
-verify_hostname HP1-dc01.muench.home.arpa # Verify return code: 0 (ok)| Endpoint | HP1 | HP2 | HP3 |
|---|---|---|---|
| LDAPS :636 | ✅ | ✅ | ✅ |
| WinRM HTTPS :5986 | ✅ | ✅ | ✅ |
| RDP certificate issued by the CA | ✅ | ✅ | ✅ |
| iLO :443 by name and by IP | ✅ | ✅ | ✅ |
Nine certificates issued so far: three for Kerberos, three for the computers, three for the iLOs. The two clients will follow on their next boot, and so will my code-signing certificate at my next logon.
🧭 What’s next
- Enforce LDAP signing and channel binding. With LDAPS in place, the last excuse is gone. It’s a single security option in the Default Domain Controllers Policy, and that option quietly overrides any registry value you set the “clever” way.
- Sign the startup scripts with the new code-signing certificate, then move the execution policy from Bypass to AllSigned.
- Windows Admin Center on the admin workstation, with a certificate from the WebServer template instead of its self-signed default.
- Two tiers, once there’s a hypervisor: an offline root VM that signs a new issuing CA and then goes back to sleep.
🎯 Takeaway
A private PKI sounded like enterprise overkill for a homelab with hardly any internal web services. In practice, the domain itself was the customer: DCs, RDP, WinRM, iLOs. The best part is that after the setup, nothing about it needs attention. Certificates appear, renew and get bound on their own. The hard part isn’t installing a CA; it’s what you don’t publish, who you don’t allow to enroll, and the flags you leave off. Security, as usual, is mostly the art of saying no. 🔏





