Windows CA Backups in Powershell

The CA is an incredibly important piece of infrastructure, especially once you start issuing your own certificates. We are pushing our code signing certs, smart card certs and certs for VPN Authentication. A loss of our CA would be a very bad day.

Backup and restoration seem simple when first checking out the Backup-CARoleService docs but there is no Microsoft documentation saying "This is everything" and that led me down a hole to find that it is indeed not everything.

C:\Windows\CAPolicy.inf controls root cert expiration length and several other critical factors, I also threw in a registry backup which I read about in this source but it does contain hostnames and such so I would not restore it on bare principle and check to see if not having the restored registry is an issue.

Important

A critical thing I learned during testing the recovery is that Certificate Templates are not stored in the CA. They are stored in AD and then replicated to all DCs and CAs. The only thing stored of the CA itself is the list of "Templates to issue" which is not very critical and is basically just a text list.

Note

I hit some of the issues that are listed in these microsoft docs so I recommend reading and being familiar with them.

I will need to run a restoration onto new bare metal to test out this process (as you should be doing anyway). I tested this in my Homelab to some degree but need to do it in a full DR.

Code

Backup

$log = 'C:\ca_backup.log'
Start-Transcript $log    

#Requires -RunAsAdministrator

# --- Initializations --- #
If (Test-Path '.\send-mail\send-mail.ps1') {
    . .\send-mail\send-mail.ps1
    } Else {
    Throw "send-mail is missing"
}

# --- Declarations --- #

#user vars, change these
$backupLocation = "\\dsk7\backups-smb\ca\"
$caBak = "C:\caBackup"
$archive = "C:\$(Get-Date -UFormat %Y-%m-%d)-$env:COMPUTERNAME.ca.zip"
$password = ConvertTo-SecureString "PASSWORD" -AsPlainText -Force

# system vars
$caPolicy = 'C:\Windows\CAPolicy.inf'
$reg = 'HKLM\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration'

# use an array to catch bad things and put it in our email
$failureArray = @()

# --- Functions --- #
Function mail {
    # stop must be here so that the file can be unlocked whenever we want to mail
    Stop-Transcript
    If ($failureArray.Count -gt 0) {
        $result = 'failure'
    } Else {
        $result = 'success'
    }
    send-mail -to 'user@contoso.com' -subject "CA Backup on $env:COMPUTERNAME $result" -body "Failures: $failureArray" -attachment $log
}

# --- Execution --- #

Write-Host "Main backup of CARoleService..." -ForegroundColor Green
Try {
    Backup-CARoleService -Path $caBak -Password $password
} Catch {
    $failureArray += 'CA backup failure'
    Write-Host $_
    mail
    Throw 'CA backup failure'
}
Write-Host "Copying CAPolicy.inf..." -ForegroundColor Green
Try {
    Copy-Item -Path $caPolicy -Destination $caBak
} Catch {
    Write-Warning 'CA policy failure'
    $failureArray += 'CA policy failure'
    Write-Host $_
}
Write-Host "Exporting issued templates..." -ForegroundColor Green
Try {
    Get-CATemplate | Export-Csv -Path "$caBak\TemplatesToIssue.csv"
} Catch {
    Write-Warning 'template failure'
    $failureArray += 'template failure'
    Write-Host $_
}
Try {
    reg export $reg "$caBak\ca-configuration.reg"
} Catch {
    Write-Warning 'registry export failure'
    $failureArray += 'registry export failure'
    Write-Host $_
}

# zip-em and move-em
$zipFiles = $caBak

Write-Host "Compressing archive $archive..." -ForegroundColor Green
Compress-Archive -Path $zipFiles -DestinationPath $archive

Write-Host "Moving $archive to $backupLocation..." -ForegroundColor Green
Try {
    Move-Item -Path $archive -Destination $backupLocation
} Catch {
    Write-Warning "move failure"
    $failureArray += 'move failure'
    Write-Host $_
}

Try {
    Remove-Item -Path $caBak -Recurse
} Catch {
    Write-Warning "failed to remove backup dir"
    $failureArray += 'backup dir failed to remove'
    Write-Host $_
}

# --- Ending Tasks --- #
mail
Note

This script utilizes my email script submodule.

Recovery

Start-Transcript 'C:\ca_recovery.log'

#Requires -RunAsAdministrator

Function ca_recovery {
    param (
        [Parameter(Mandatory=$True)]
        [Object]$archive
    )
    
    # use vars
    $caBak = "C:\caBackup"
    $password = ConvertTo-SecureString "PASSWORD" -AsPlainText -Force

    # system vars
    $caPolicy = 'C:\Windows\CAPolicy.inf'
    $reg = 'HKLM\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration'

    If (!(Test-Path $archive)) {
        Throw "$archive does not exist, check the path and try again"
    }

    If (Test-Path $caBak) {
        Throw "$caBak exists, verify your goal and remove it"
    }
    Write-Host "Expanding $archive into C:\ it will be $cabBak" -ForegroundColor Green
    Expand-Archive -Path $archive -DestinationPath 'C:\'
    
    # start restoring things
    # we need the service stopped before we can do anything
    # not stopping is terminal
    Write-HOst "Restoring DB and Keys..." -ForegroundColor Green
    Try {
        Stop-Service CertSvc
    } Catch {
        Throw "Failed to stop CertSvc service"
        Write-Host $_
    }

    # To prevent issues this command does not have -Force by default
    # to overwrite an existing CA you must add the -Force flag or
    # swap the two commands below and use the commented one
    Try {
        Restore-CARoleService -Path $caBak -Password $password
        # Restore-CARoleService -Path $caBak -Password $password -Force
    } Catch {
        Throw "Restoration failed, this could be because data exists. If you are overwriting then edit this script to include the -Force flag on Restore-CARoleService"
        Write-Host $_
    }
    
    # import the capolicy.inf if it does not exist, we do not force overwrites
    If (Test-Path $caPolicy) {
        Write-Warning "$caPolicy exists, we will not overwrite it. If you wish to you can manually copy $caBackup\CAPolicy.inf"
    } Else {
        Copy-Item -Path "$caBak\CaPolicy.inf" -Destination $caPolicy
    }

    # import our issue templates
    # nothing bad can happen, so we force it
    Write-Host "To import the previous Templates to issue run the following after starting the CertSvc service" -ForegroundColor Green
    Write-Host '$caBak ='$caBak
    Write-Host '$templates = Import-Csv "$caBak\TemplatesToIssue.csv"'
    Write-Host '$templates | Add-CATemplate -Force'
    
    # handle registry notice, we won't do this automatically because bad stuff can happen
    # this contains things like hostnames and could be dangerous to over-write
    Write-Warning "Please manually import $caBak\ca-configuration.reg if you need to. BACKUP THE EXISITNG REGISTRY FIRST."
    
    # No cleanup to do, we just have notices.
    Write-Host "We are leaving $caBak in place. This dir contains your Templates, registry and CAPolicy.inf." -ForegroundColor Green
    Write-Host "This script has finished." -ForgroundColor Green
 
}
ca_recovery

Stop-Transcript

References

Official docs

Backup ideas and tips

Cert Templates are stored in AD