The project started with a Linux problem: how do I give systems such as Proxmox, PBS, and NGINX the same kind of automated certificate lifecycle I expect from public ACME services, while keeping Microsoft AD CS as the internal certificate authority?
The answer turned out to be more general than the original problem. By placing an ACME-compatible service in front of AD CS, the resulting architecture can serve any compatible ACME client—not just Linux.
The finished path keeps those responsibilities deliberately separate: ACME clients request and renew certificates through NGINX and acme2certifier, while Microsoft AD CS remains the authority that applies certificate policy and signs the result. The rest of this article shows how that bridge was built, how Proxmox consumed it natively, and why validating the revocation path became just as important as validating issuance. The architecture overview below shows the complete path at a glance.

1. Starting with Microsoft AD CS
Tall Grass Compute already had an internal Microsoft PKI in place before this project started. The design follows a traditional two-tier model: an offline standalone root CA and an online enterprise issuing CA.
TGC-AD-CA01 hosts the TGC Root CA and remains offline during normal operation. Day-to-day issuance belongs to TGC-AD-PKI01, which runs the enterprise subordinate CA TGC Issuing CA 01. The ACME service never needs to interact directly with the offline root.
The certificate template is intentionally narrow. TGC Web Server carries the Server Authentication EKU, allows the requester to supply the subject and SAN, uses a two-year validity period, and is the only template exposed through the final ACME integration.
The environment also separates infrastructure identity from TLS service identity. For example, the Proxmox host is TGC-PVE-01.tallgrasscompute.local, while its web certificate is issued to pve-01.tallgrasscompute.com. Those names are intentionally different: one identifies the underlying infrastructure host; the other identifies the TLS service clients connect to.

2. Why ACME Was the Missing Piece
Tall Grass Compute was already using ACME to automate public certificates, so the workflow was familiar and proven. The internal PKI was different. Microsoft AD CS provided the trust model I wanted, but Proxmox, PBS, NGINX, and other Linux-based systems are much more naturally aligned with ACME than with Microsoft certificate-enrollment protocols.
If ACME already worked well for public certificate automation, why not use the same model internally while keeping Microsoft AD CS as the certificate authority?
The goal was not to replace AD CS. It was to put an ACME-compatible interface in front of it.
That way, certificate consumers can continue using the protocol they already understand, while AD CS remains responsible for issuance, policy, trust, and the certificate lifecycle behind the scenes.
3. Building the Bridge Between ACME and AD CS
acme2certifier became the translation layer. From a Proxmox node, the environment looks like a normal ACME service. Behind that endpoint, acme2certifier authenticates to Active Directory with a Kerberos keytab and submits requests to TGC Issuing CA 01 through MS-ICPR/RPC.
acme2certifier uses a dedicated Active Directory service account and Kerberos authentication to submit certificate requests to the issuing CA.

4. Configuring acme2certifier for AD CS
The core Microsoft integration lives in the CAhandler section of acme2certifier. The important pieces are the issuing CA, the TGCWebServer template, Kerberos authentication, and the explicit boundaries around what the gateway is allowed to request.
[CAhandler]
handler_module: acme2certifier.cahandlers.msicpr_ca_handler
host: TGC-AD-PKI01.tallgrasscompute.local
target_domain: TALLGRASSCOMPUTE.LOCAL
domain_controller: TGC-AD-DC01.tallgrasscompute.local
ca_name: TGC Issuing CA 01
template: TGCWebServer
allowed_templates: ["TGCWebServer"]
allowed_domainlist: ["tallgrasscompute.com", "*.tallgrasscompute.com"]
use_kerberos: True
krb5_auth_backend: python
krb5_principal: svc-ces@TALLGRASSCOMPUTE.LOCAL
krb5_keytab: /run/secrets/svc-ces.keytab
timeout: 10Those restrictions matter. The bridge is not being handed unrestricted enrollment capability. The template is fixed, and requested names are constrained to the service namespace used by the lab.
NGINX sits in front of the container at https://acme.tallgrasscompute.com. It terminates TLS, applies internal network restrictions, and proxies the ACME service to localhost:22280. HTTP-01 challenge traffic is handled separately under /.well-known/acme-challenge/.
5. Evidence on the Issuing CA

The issuing-CA console provides useful proof that the translation really ends in Microsoft AD CS. Requests for acme.tallgrasscompute.com, PBS, ProxCenter, PDM, PVE-01, and PVE-02 were issued through the TGC Web Server template under the service identity used by the gateway.
6. Using Proxmox as the First ACME Client
Proxmox was a good first client because it already includes native ACME support. There is no custom certificate-deployment script on the node. Proxmox points its ACME account at the internal directory, requests the certificate, completes validation, installs the result, and restarts pveproxy itself.
acme: account=tgc-acme
acmedomain0: pve-01.tallgrasscompute.com
Directory:
https://acme.tallgrasscompute.com/directoryThe important part is what did not change: Proxmox still speaks ACME. The fact that Microsoft AD CS ultimately signs the certificate is an implementation detail behind the ACME gateway.

The First Full End-to-End Order
To prove the complete path, I forced a new certificate order on TGC-PVE-01.
The output shows the authorization moving from pending to valid, CSR creation, order finalization, certificate download, installation into pveproxy, and the pveproxy restart.

This is the point where the architecture stopped being theoretical. A native Proxmox ACME client had completed a normal ACME transaction, while Microsoft AD CS handled the actual certificate issuance behind the scenes.
7. Trust, But Verify
A successful ACME task tells me the client completed its workflow. It does not, by itself, prove that the running service is presenting the certificate I expect or that the complete trust and revocation path works.
That distinction matters because certificate automation has several checkpoints. A successful order proves that the ACME transaction completed; it does not prove that the new certificate was installed, that the live listener is presenting it, or that clients can build and validate the full trust and revocation path. Each of those outcomes needs to be verified independently.
For that reason, I treated runtime verification as a separate phase of the automation rather than an implicit side effect of issuance. The checks needed to answer three different questions: did AD CS issue the expected certificate, did Proxmox actually install and present it, and could a client validate the complete chain and revocation path from the network?
After the forced order, the installed pveproxy certificate and the certificate presented remotely on TCP 8006 had the same SHA-256 fingerprint. That closed the operational loop from order to runtime presentation.
The certificate is not “done” because the order succeeded. It is done when the service is presenting the expected certificate and clients can validate it.

End-to-End Validation with CRL Checking
I then validated the live Proxmox endpoint with the TGC root explicitly trusted and CRL checking enabled. This moved the test beyond “the browser likes it” and into the actual PKI mechanics.

The certificate was being served with TLS 1.3, used a 4096-bit RSA key, chained to TGC Issuing CA 01, and passed revocation-aware validation.
That test exposed the most important operational gap in the project.
8. The Part I Almost Missed: CRL Publication
The certificate was valid. The chain was correct. ACME issuance worked. But when I inspected the CRL Distribution Point embedded in the certificate, the CRL being served from pki.tallgrasscompute.com was stale.
The URL was correct and the file existed. The problem was operational: the published issuing-CA CRL had passed its NextUpdate time. That meant certificate issuance could succeed while revocation-aware validation eventually failed.
That discovery changed the finish line again. A complete certificate lifecycle could not stop at successful issuance; the revocation path also had to stay current, reachable, and independently verifiable. I treated CRL publication as a second automation workflow rather than a simple file-copy task.

The finished job runs on TGC-AD-PKI01 as SYSTEM. It generates fresh CRLs with certutil -crl, transfers them through a dedicated ED25519 key, downloads the HTTP-served copy, compares SHA-256 hashes, logs the result, and exits non-zero if publication verification fails.
The Linux-side key is restricted to the issuing CA source address and forced into SFTP-only operation. The NGINX host key is pinned. The publication account has one job: place PKI publication material in the web root.
Online Issuing CA vs. Offline Root
The issuing CA and root CA deliberately follow different operational models.
TGC Issuing CA 01 is online because it exists to perform routine certificate operations, so automated CRL publication makes sense. The root CA remains offline, and its CRL publication stays controlled and manual.
Online issuing CA: automate routine PKI operations. Offline root CA: preserve the isolation that makes it a root of trust.
9. A Few Paths We Did Not Keep
Before settling on acme2certifier, I tested other approaches, including direct certificate enrollment from Linux and Microsoft CEP/CES. Those experiments proved individual pieces of the workflow, but they also added complexity that did not fit the final goal.
The design became much cleaner once I stopped trying to make every Linux system understand Microsoft enrollment protocols directly. The gateway became the translation layer instead: clients speak ACME, while acme2certifier handles Microsoft-specific enrollment behind the scenes.
Once that direction was clear, the temporary CEP/CES components, the client-authentication template created for that experiment, and other abandoned enrollment tooling were removed.
10. Lessons Learned
Use the protocol the clients already understand.
Proxmox already knows ACME. So do many other infrastructure platforms. The better design was not to teach every client Microsoft PKI enrollment; it was to centralize that translation in one place.
Centralize Microsoft-specific complexity.
Kerberos, MS-ICPR, AD identities, certificate templates, and CA policy all live behind the gateway. Individual clients need only the ACME directory and the service name they are requesting.
Restrict the bridge aggressively.
The service identity, certificate template, DNS namespace, network access, keytab mount, and CRL publication key are all deliberately constrained. Automation connected to a certificate authority should have the smallest useful blast radius.
Issuance is only part of PKI operations.
The stale CRL was the most useful discovery in the build. A certificate can be issued correctly while the revocation infrastructure behind it is unhealthy. AIA and CDP publication are part of the lifecycle, not optional documentation details.
Validate runtime state, not configuration intent.
The final tests checked the certificate actually presented by pveproxy, the chain to the trusted root, and the published CRL. The same philosophy has shown up repeatedly in Tall Grass Compute projects: configuration state tells you what should be true; runtime validation tells you what actually is true.
11. Final Result
The original objective was to automate internal certificates for Linux infrastructure. The finished design goes further: Tall Grass Compute now has an internal ACME service backed by Microsoft AD CS.
From the client side, the experience is ACME. From the PKI side, the authority is still Microsoft AD CS.

Proxmox was the first client used to prove the design, but the architecture is not tied to Proxmox—or even to Linux. Any compatible ACME client that meets the policy and validation requirements can use the same service.
The project started as a Linux certificate problem. It ended as a reusable bridge between two certificate ecosystems already present in the lab.
The final architecture brings the enrollment, deployment, publication, and validation paths together into one internal certificate lifecycle.
Build it. Break it. Find out what actually happens.
That is exactly what happened here. ACME issuance worked. Then deeper validation found a stale CRL. The publication path was automated and hardened. Finally, the complete service was tested again with live chain and revocation checking enabled.
The result is not merely a certificate that can be issued. It is a certificate lifecycle I can trust end to end.
What Comes Next
The architecture is now proven with Proxmox, but the useful part is that nothing about the gateway is Proxmox-specific. The same internal ACME directory can become the enrollment point for other compatible services in the lab as their certificate lifecycles are moved onto the same model.
The next phase is therefore operational rather than architectural: onboard additional internal services, confirm unattended renewal behavior, and keep validating the pieces that live outside the certificate itself - the listener, the trust chain, AIA, and current CRLs. The bridge is built; the job now is to use it consistently.
That is the end state I wanted from the beginning: Microsoft AD CS remains the internal authority, while the systems consuming its certificates get the simple, repeatable ACME experience they already understand.

Comments
Questions, feedback, or something you tried differently? Leave a comment below.