[P 2.1] Build Sheet: The Offline Root CA

By the end of this you have a standalone offline root certificate authority built, its policy file and distribution points fixed before anything is signed, its first revocation list published with a year of margin, and its certificate and CRL exported ready for the web servers.


By the end of this article you have a standalone root certificate authority built and shut down, with its policy file, its distribution points and its revocation schedule all fixed before it signed anything, and its certificate and first revocation list exported ready for the web servers you build next.

Most steps here are not recoverable. A mistake in the policy file, the database path, the common name or the distribution point configuration is corrected by reinstalling the role and starting again, and if the mistake is discovered after this authority has signed the certificate below it, the correction is a rebuild of the hierarchy. Read each step before running it. Every step gives both a walk-through in named product terms and the equivalent command, and the two produce identical results.

Prerequisites. A virtual machine running Windows Server 2022 or 2025 Standard with the desktop experience, named ROOTCA, joined to a workgroup and not to a domain, with no network adapter attached. A second volume presented as D: with at least forty gigabytes free. A local Administrator password of at least sixteen characters, stored somewhere you will still have access to in a decade. A tested file transfer path in and out of the machine through the hypervisor’s shared folder, presented as E: and holding a folder named RootExport, since there is no network path. Windows fully updated before the role is installed, because patching it afterwards means attaching an adapter to a machine that is not supposed to have one. You are logged in as the local Administrator throughout, and whoami returns rootca\administrator.

The naming convention this series uses. Hostnames are role-based, uppercase and unqualified: ROOTCA, SUBCA, PKI-WEB01 and PKI-WEB02. Certificate authority common names are organisation-scoped and describe the role, never the host: CatSnackJack Root CA and CatSnackJack Issuing CA. Keeping the hostname out of the common name matters, because the common name appears in every certificate the hierarchy ever issues, and there is no reason to publish the name of a machine that is supposed to be unreachable. Working folders on the root mirror the extensions they serve, under a single parent on the data volume.


Step 1. Create the folder structure

The database path is supplied at configuration time and the directory must already exist. Certificate Services will not create it, and the configuration wizard fails at the last screen if it is missing, which is an irritating place to discover a typo.

$folders = @(
    "D:\RootCA\AIA",
    "D:\RootCA\CDP",
    "D:\RootCA\DB",
    "D:\RootCA\Certs",
    "D:\RootCA\CSR"
)
foreach ($folder in $folders) { New-Item -ItemType Directory -Path $folder -Force }
Get-ChildItem D:\RootCA

Expected result: five directories listed. AIA and CDP hold the files the authority publishes, DB holds the certificate database and its transaction log, Certs holds the exported root certificate, and CSR is where the request from the issuing authority arrives when you come back to this machine in a later article.


Step 2. Write CAPolicy.inf before anything else

This file controls how the authority is configured at installation, and it is read once. It must be at C:\Windows\CAPolicy.inf before the role is configured. If it is absent or misnamed, installation proceeds silently with defaults and there is no warning of any kind.

The single most common failure on this step is Notepad saving the file as CAPolicy.inf.txt because the extension was hidden. The build then completes, looks correct, and has ignored every setting in the file. Creating the file from the command line removes the possibility entirely, which is the only reason I do it that way.

$policy = @"
[Version]
Signature="$Windows NT$"

[PolicyStatementExtension]
Policies = AllIssuancePolicy, InternalPolicy
Critical = FALSE

[AllIssuancePolicy]
OID = 2.5.29.32.0

[InternalPolicy]
OID = 1.2.3.4.1455.67.89.5
Notice = "CatSnackJack Certificate Authority - Internal Use Only"

[Certsrv_Server]
RenewalKeyLength = 4096
RenewalValidityPeriod = Years
RenewalValidityPeriodUnits = 25
AlternateSignatureAlgorithm = 0
LoadDefaultTemplates = 0
CRLPeriod = Weeks
CRLPeriodUnits = 52
CRLDeltaPeriod = Days
CRLDeltaPeriodUnits = 0

[CRLDistributionPoint]

[AuthorityInformationAccess]
"@

$policy | Out-File -FilePath "C:\Windows\CAPolicy.inf" -Encoding ASCII
Get-Item "C:\Windows\CAPolicy.inf" | Select-Object Name, Length
Get-Content "C:\Windows\CAPolicy.inf"

Expected result: the file listed with the name CAPolicy.inf exactly, and its contents echoed back. If the name shows an additional extension, delete it and run the block again rather than renaming it, because a renamed file created by Notepad may also carry the wrong encoding.

Every directive in that file earns its place, and several are easy to misread.

SettingValueWhy
RenewalKeyLength4096Key size used when this certificate is renewed, not at installation. The installation key size is chosen in the configuration wizard. Setting one and not the other is how hierarchies end up renewing at a different strength than they were built.
RenewalValidityPeriod / UnitsYears / 25Lifetime applied when the root certificate is renewed. This directive applies to a root only. On a subordinate it is inert, because a subordinate’s lifetime is set by whoever signs it.
AlternateSignatureAlgorithm0Leaves signatures in the classic PKCS format. Setting it to 1 switches to RSASSA-PSS padding, which some appliances and older trust stores cannot parse. This is the certificate every relying party in the estate must read, so choose the format with the widest reach.
LoadDefaultTemplates0Documents intent. It has no effect here, because it applies to enterprise installations and a standalone authority has no templates to load. Leave it in as a statement that this authority signs one thing, and place no reliance on it.
CRLPeriod / UnitsWeeks / 52Mirrors the working schedule set by registry in step 5, and does so deliberately. The policy file is read again when the root certificate is renewed, not only at installation, so a value here that disagrees with the registry is one somebody has to reason about years later during a procedure they perform once. Matching them removes the question. It also means a rebuild that skips step 5 lands on fifty-two weeks rather than the one week installation default, which on a machine about to be powered off and put in a safe is the failure worth engineering out.
CRLDeltaPeriodUnits0Disables delta revocation lists. A root revokes one certificate class approximately never, so a delta location would be an advertised endpoint that is never populated.
[CRLDistributionPoint] and [AuthorityInformationAccess]present, emptyEmpty sections cause those extensions to be omitted from the root’s own self-signed certificate. Nothing performs revocation checking on a root, so an extension here can only point somewhere useless.

Two of those sit one step apart from something they are frequently confused with. The empty distribution point and authority access sections govern the root’s own certificate. The commands in step 6 govern what the root writes into the certificate it issues. Same two acronyms, two different objects, and reversing them produces an issuing authority whose distribution points point nowhere.

The policy OID shown is a Microsoft sample and is safe for internal use. If you want a policy identifier that is genuinely yours, a Private Enterprise Number is free from IANA and yields an arc of the form 1.3.6.1.4.1.. The notice text and the practice statement URL both surface to anyone who inspects a certificate and clicks through to the issuer statement, so write them as though a customer’s auditor will read them, because eventually one will.


Step 3. Install the role binaries

In Server Manager, choose Add Roles and Features, take the role-based installation, select ROOTCA, and on the Server Roles page select Active Directory Certificate Services, accepting the prompt to add the required management features. On the Role Services page select Certification Authority and nothing else. Do not select Web Enrollment, the Online Responder, or any other service; this machine has no network and will never serve a request. Complete the wizard and stop at the end. Do not follow the link to configure the role yet.

Add-WindowsFeature -Name ADCS-Cert-Authority -IncludeManagementTools
Get-WindowsFeature ADCS-Cert-Authority | Select-Object Name, InstallState

Expected result: InstallState reads Installed. No restart is required. -IncludeManagementTools is what gives you the Certification Authority console and certutil; omit it and you have a role you cannot administer locally, on a machine with no remote management path.


Step 4. Configure the authority

This is the irreversible step. Every value below is embedded into the root certificate at creation and cannot be altered afterwards without reinstalling the role and rebuilding everything beneath it.

SettingValueWhy
Setup TypeStandalone CARequired, because ROOTCA is not domain joined. Enterprise requires directory connectivity this machine deliberately does not have.
CA TypeRoot CAThe trust anchor. It issues exactly one certificate in its life.
Private KeyCreate a new private keyA new hierarchy. The alternative applies only when restoring or renewing an existing authority.
Cryptographic ProviderRSA#Microsoft Software Key Storage ProviderThe software provider. Substitute the vendor provider here if a hardware module is in use, and note that the module must be installed and configured before this point.
Key Length4096Permanent. Appropriate for a certificate that must remain credible for decades.
Hash AlgorithmSHA256Permanent. The current standard. Longer digests offer no practical benefit here and reduce compatibility.
Validity Period25 YearsThe life of the root certificate itself. Plan the renewal conversation at roughly half of it.
Certificate Database LocationD:\RootCA\DBChosen at configuration. Relocatable later, at the cost of an outage.
Certificate Database Log LocationD:\RootCA\DBCo-located deliberately. Write activity on a root is negligible, so separating the log buys nothing.

In Server Manager, open the post-deployment configuration for Active Directory Certificate Services, confirm the local Administrator on the Credentials page, select Certification Authority on Role Services, then work through Setup Type, CA Type, Private Key, Cryptography, CA Name, Validity Period and Certificate Database using the values above. The common name field is the one to slow down on. CatSnackJack Root CA appears in the Issued By field of every certificate this hierarchy will ever produce, and there is no rename.

Install-AdcsCertificationAuthority `
  -CAType StandaloneRootCA `
  -CACommonName "CatSnackJack Root CA" `
  -CryptoProviderName "RSA#Microsoft Software Key Storage Provider" `
  -KeyLength 4096 `
  -HashAlgorithmName SHA256 `
  -ValidityPeriod Years -ValidityPeriodUnits 25 `
  -DatabaseDirectory "D:\RootCA\DB" `
  -LogDirectory "D:\RootCA\DB" `
  -Force

Expected result: the cmdlet returns an ErrorId of 0. Failure mode to watch for: if the database directory does not exist the command fails at the end rather than the beginning, after the key has already been generated, and the cleanest recovery is to remove the role and start the step again.


Step 5. Set the revocation schedule and the issued-certificate lifetime

These values live in the registry under the authority’s configuration key and there is no meaningful console equivalent for most of them.

certutil.exe -setreg CA\DSConfigDN "CN=Configuration,DC=catsnackjack,DC=com"

certutil.exe -setreg CA\ValidityPeriodUnits 5
certutil.exe -setreg CA\ValidityPeriod "Years"

certutil.exe -setreg CA\CRLPeriodUnits 52
certutil.exe -setreg CA\CRLPeriod "Weeks"

certutil.exe -setreg CA\CRLOverlapUnits 6
certutil.exe -setreg CA\CRLOverlapPeriod "Weeks"

certutil.exe -setreg CA\CRLDeltaPeriodUnits 0

net stop CertSvc && net start CertSvc
certutil.exe -getreg CA\CRLPeriodUnits
certutil.exe -getreg CA\ValidityPeriodUnits

DSConfigDN tells this authority the distinguished name of the forest configuration partition, which it has no way to discover because it cannot see the directory. The value is written into the certificate it issues, and it is what allows domain-joined clients to resolve directory-based paths later. It is also the %6 token that appears in the directory paths on the issuing authority, so getting it right here is what makes those work there.

Substitute your own forest root, remembering that this is a distinguished name rather than a domain name: every label separated by a dot becomes its own domain component, in the same order. A forest rooted at corp.example.com gives CN=Configuration,DC=corp,DC=example,DC=com. Worth stating plainly because a subdomain of a registered public name is what Microsoft recommends for an Active Directory namespace, so the three-component form is the one most readers will need.

ValidityPeriod here is the maximum life of certificates this authority issues, which means the issuing authority’s certificate and nothing else. Five years is the figure this estate uses. This is the setting that actually controls that lifetime; a corresponding directive in the subordinate’s own policy file does not, and expecting it to is a common and confusing error.

The revocation list runs for fifty-two weeks with a six week overlap. The overlap is the margin, and it is the difference between an annual task with slack and an annual task with a cliff. Six weeks means the revocation list published this year remains acceptable for six weeks past the date its replacement was due, so a missed reminder, a holiday or an incident does not become an outage. Nothing about a longer overlap costs anything. The file is the same size, the publication schedule is unchanged, and the only effect is that the previous list stays acceptable for longer, which is precisely the property you are buying. Be generous with it.


Step 6. Fix the distribution points before anything is signed

Each of these two registry values is a list, and each entry in a list does one of two quite different jobs depending on its numeric flag. Some entries mean write the file to this location, and no client ever sees them. Others mean put this URL into the extension of every certificate I issue. One value, two jobs, interleaved. Keeping that distinction in mind is what stops the rest of this step looking arbitrary.

Installation seeds four entries in each value, and only one of them is merely unwanted. The local CertEnroll folder under the system directory is harmless, just not where this build keeps its files. The other three are the problem: a directory path, which on a workgroup machine points into a directory that has never heard of this authority; an HTTP URL built from the machine’s own name, which resolves to http://ROOTCA/; and a UNC path to the same machine. That machine is about to be switched off for a decade.

This is not tidying. Two of those defaults carry the flag that writes them into issued certificates, so if they survive this step they are frozen into the subordinate CA certificate, and every client validating anything in this hierarchy for the next five years tries an unreachable location and waits for it to fail before reaching the one that works. The UNC entry is the worst, because a dead SMB path takes the longest to give up. Clear both values and rebuild them.

certutil.exe -setreg CA\CRLPublicationURLs ""
certutil.exe -setreg CA\CACertPublicationURLs ""
net stop CertSvc && net start CertSvc

Then set the two entries each extension needs: a local path where the authority writes the file, and the public URL that gets written into the certificate it issues.

certutil.exe -setreg CA\CRLPublicationURLs "1:D:\RootCA\CDP\%3%8%9.crl\n6:http://pki.catsnackjack.com/pki/%3%8%9.crl"

certutil.exe -setreg CA\CACertPublicationURLs "1:D:\RootCA\AIA\%1_%3%4.crt\n2:http://pki.catsnackjack.com/pki/%1_%3%4.crt"

net stop CertSvc && net start CertSvc
certutil.exe -getreg CA\CRLPublicationURLs
certutil.exe -getreg CA\CACertPublicationURLs

The percent tokens are substituted by the authority at publication time. %3 is the sanitised authority name, %8 a suffix that increments when the revocation list key changes, %9 a marker that distinguishes a delta list from a base list, %1 the server’s DNS name and %4 a suffix that increments when the certificate is renewed. Writing literal filenames instead of tokens works right up until the first renewal, at which point the authority publishes to a name nothing is looking for.

Two consequences of those tokens are worth seeing before you move on, because both are visible in the finished URLs and neither is a defect. %1 expands to this machine’s name, so the published certificate file is ROOTCA_CatSnackJack Root CA.crt and the authority access URL inside the subordinate certificate therefore contains the string ROOTCA. That sits slightly awkwardly beside the naming argument at the top of this article, which was that the authority’s own name should not carry the hostname. The convention is standard and I keep it, but it is worth knowing that the hostname reaches the outside world through the filename even when it is absent from the common name. If that bothers you, drop %1 from both certificate paths and use %3%4.crt, which is equally valid and slightly less informative when you are looking at a directory full of files years later.

The other is that %3 sanitises the authority name for use in a filename but does not remove spaces, so the resulting URL carries percent-encoded spaces. That is correct, every client handles it, and it looks like a mistake the first time you see it in a certificate. Leave it alone.

The numeric prefix on each entry is a bit mask, and this is the part of certificate authority configuration most likely to be explained incorrectly, including by tables that circulate widely and by more than one guide I have written myself. The two registry values do not share a bit vocabulary, which is the root of the confusion. In the revocation list value, 1 publishes the base list to that location and 2 places the URL into the distribution point extension of issued certificates, so the 6 above combines that with the extension that advertises a freshest-list location. In the certificate value, 1 publishes the certificate file to that location and 2 places the URL into the authority information access extension of issued certificates. Rather than trusting any table, including this paragraph, run the verification commands above: -getreg prints the flag names in plain language alongside the values, and that output is the arbiter.

Modifying a distribution point later affects only newly issued certificates. Everything already signed keeps asking for the old location, forever.

Note that the URL is HTTP and not HTTPS, deliberately. A distribution point served over HTTPS creates a circular dependency, because validating the certificate protecting that connection requires fetching a revocation list. Neither extension supports HTTPS, and clients that try will fail in ways that are tedious to diagnose.

You can confirm the result in the Certification Authority console under the authority’s Properties, on the Extensions tab, by selecting each extension in turn and checking that the local path carries no publication checkboxes beyond writing the file, and that the HTTP entry carries the include-in-issued-certificates option. That is verification, not configuration; the commands above have already done the work.


Step 7. Turn on auditing, then publish the first revocation list

certutil.exe -setreg CA\AuditFilter 127
net stop CertSvc && net start CertSvc
certutil.exe -crl
Get-ChildItem D:\RootCA\CDP

Expected result: a .crl file in the distribution folder. Failure mode worth knowing: the audit filter alone does not produce events. Certificate Services auditing requires the corresponding object access audit policy to be enabled as well, and with only one of the two switched on you get a configuration that looks audited and logs nothing. On a machine that is switched off for most of its life this matters less than on the issuing authority, but set it here so the habit is consistent.

Inspect what you just published, because the dates are the point of the whole exercise.

certutil.exe -dump D:\RootCA\CDP\*.crl

Read the ThisUpdate and NextUpdate fields. NextUpdate should sit roughly fifty-eight weeks out: fifty-two weeks of publication period plus the six week overlap. If it reads twelve hours, or a few days, the overlap did not apply and the service was probably not restarted after the registry was written.


Step 8. Export, verify, and shut down

Copy the root certificate and the revocation list out through the hypervisor shared folder, which this build presents to the root as E:\RootExport\. Both are needed by the web servers in the next article, and the certificate is needed again by the issuing authority after that. Both later articles read that folder by name, so use it verbatim rather than substituting your own.

Copy-Item C:\Windows\System32\CertSrv\CertEnroll\*.crt D:\RootCA\Certs\
Get-ChildItem D:\RootCA\Certs, D:\RootCA\CDP

certutil.exe -dump D:\RootCA\Certs\*.crt
certutil.exe -verify D:\RootCA\Certs\*.crt

Copy-Item D:\RootCA\Certs\*.crt E:\RootExport\
Copy-Item D:\RootCA\CDP\*.crl E:\RootExport\

The end-to-end test for this article. Dump the root certificate and confirm four things in the output: the Subject reads CN=CatSnackJack Root CA, the validity spans twenty-five years, the public key is RSA 4096 with a SHA-256 signature, and there is no CRL Distribution Point or Authority Information Access extension present on the certificate itself. That last one is the empty policy file sections doing their job, and its absence is the correct result. Then confirm the revocation list dumps with a NextUpdate roughly fifty-eight weeks out, and that the two exported files are readable from the machine you copied them to.

What you cannot test yet is the thing that matters most: whether the URLs written into the configuration actually resolve and serve those files. They do not, because the web servers behind pki.catsnackjack.com do not exist. That is the next article, and its end-to-end test is the one that closes this loop.

Then shut the machine down. Not suspended, not left running with the adapter removed. Shut down, with the disk exported to encrypted removable media if that is your storage model, and with the local Administrator password recorded somewhere durable and unrelated to the estate this authority serves. If you store that password in a system that authenticates using a certificate from this hierarchy, you have built a loop that will be discovered at the worst imaginable moment.


A worked example: the appointment you just created

Suppose you completed this build on the first of August 2026. The revocation list you published carries a NextUpdate around the tenth of September 2027, being fifty-two weeks plus the six week overlap. The date that matters operationally is not that one. It is the first of August 2027, when the publication period expires and the six weeks of margin begin to burn.

So the calendar entry goes in for mid-July 2027, two weeks ahead of the publication date, owned by a named role rather than a named person, and it reads as a task rather than a reminder: locate the root, restore it, correct the clock, publish a new revocation list, copy it to both web servers, confirm it is being served over HTTP from outside the network, and shut the machine down again. That task has a hard failure date of the tenth of September 2027, and everyone involved should know it. Set the same entry to repeat annually, and put the hard date in the description so that whoever inherits it in five years understands what they are looking at.

Had you accepted a three day overlap instead of six weeks, that same missed reminder would have given you until the fourth of August. The build is otherwise identical. The difference is entirely in how much room you left yourself, and it costs nothing to be generous.


Rollback

Before this authority has signed anything, rollback is clean: remove the role, delete the working folders, delete the policy file, and start again. Nothing outside the machine has been affected because nothing outside the machine has seen it.

Uninstall-AdcsCertificationAuthority -Force
Remove-WindowsFeature ADCS-Cert-Authority
Remove-Item C:\Windows\CAPolicy.inf
Remove-Item D:\RootCA -Recurse -Force

After it has signed the issuing authority’s certificate, there is no rollback in any meaningful sense. The correction is a new hierarchy and a trust migration. That asymmetry is the reason this article spends more words on a policy file than on the installation itself.


The root is built and put away. It has published a revocation list that nothing can currently reach, and it has recorded a set of URLs it expects to exist. Making those URLs real, on two web servers behind one name, is the next article, and it is the one I would argue determines whether this hierarchy survives contact with an actual estate.


On-Prem PKI
‹ Previous: [P 2] The Offline Root: Designing for a Machine That Is Almost Never On
Next: [P 3] Revocation Is a Distribution Problem