Bulk Create Cloudflare Origin CA Certificates with PowerShell
Cloudflare Origin CA certificates are useful when your origin server only receives traffic through proxied Cloudflare records. They encrypt traffic between Cloudflare and your origin server and can be used with Cloudflare's **Full (strict)** SSL/TLS mode.
This PowerShell script bulk-creates Cloudflare Origin CA certificates for a list of domains. For each domain, it can create a local private key and CSR with OpenSSL, submit the CSR to Cloudflare, save the returned PEM certificate, and write a CSV summary.
Origin CA certificates are not publicly trusted browser certificates. They are meant for the connection between Cloudflare and your origin. If you pause Cloudflare or turn off proxying for a hostname that uses a Cloudflare Origin CA certificate, visitors may see certificate trust errors.
What the Script Does
The Generate-CloudflareOriginCerts.ps1 script:
- Requires PowerShell 7 or higher.
- Supports Cloudflare API Token, Origin CA Key, or Global API Key authentication.
- Creates Cloudflare API request headers based on the selected auth mode.
- Creates an output folder for certificate files.
- Finds OpenSSL from
PATH, Git for Windows, or common OpenSSL install folders. - Generates a private key and CSR locally, or copies an existing CSR.
- Requests an Origin CA certificate from Cloudflare.
- Saves the certificate as a
.pemfile. - Optionally removes the CSR after certificate creation.
- Optionally revokes matching old Cloudflare Origin CA certificates.
- Exports a CSV summary with domain, file paths, certificate ID, expiration date, request type, and validity.
Configure Cloudflare Authentication
For new automation, set the script to use an API token:
$CloudflareAuthMode = "ApiToken" $CloudflareApiKey = "PUT_YOUR_CLOUDFLARE_API_TOKEN_HERE"
Cloudflare documents the required permission for Origin CA certificate API calls as:
Zone - SSL and Certificates - Edit
Scope the token to the zones you need instead of giving it broader account access.
If you use API token mode, you do not need to set a Cloudflare email address.
The script also supports GlobalApiKey mode. Use this only when you specifically need Global API Key authentication. In this mode, the Cloudflare email address is required because Cloudflare expects both X-Auth-Email and X-Auth-Key headers:
$CloudflareAuthMode = "GlobalApiKey" $CloudflareEmail = "[email protected]" $CloudflareApiKey = "PUT_YOUR_CLOUDFLARE_GLOBAL_API_KEY_HERE"
The email address must be the Cloudflare user email that owns the Global API Key.
The script can also use OriginCAKey mode with X-Auth-User-Service-Key, but Origin CA service keys are deprecated by Cloudflare and are scheduled for removal on September 30, 2026. Use an API token when you can.
Configure the Domains
Edit the $Domains array and replace the placeholder domains:
$Domains = @(
"example.com",
"example.net",
"example.org"
)
For each domain, the script requests a certificate covering the apex and one wildcard:
example.com *.example.com
Cloudflare allows Origin CA certificate hostnames to include fully qualified domain names and wildcards. Wildcards only cover one hostname level, so *.example.com covers www.example.com but not app.secure.example.com.
Configure the Output Folder
Set the folder where the script should write keys, CSRs, PEM files, and the summary CSV:
$CertOutputPath = "C:\DevOps\Certs"
The script creates the folder if it does not already exist.
For a domain named example.com, the output files normally look like this:
C:\DevOps\Certs\example.com.key C:\DevOps\Certs\example.com.csr C:\DevOps\Certs\example.com.pem
If files already exist and $RemoveOldLocalFiles is $false, the script creates timestamped filenames instead of overwriting them.
Choose the Certificate Request Mode
To generate the private key and CSR locally with OpenSSL, use:
$CertificateRequestMode = "LocalOpenSsl"
This is the normal mode when you want the script to create one key and CSR per domain.
To use an existing CSR file, use:
$CertificateRequestMode = "ExistingCsr" $ExistingCsrPath = "C:\DevOps\Certs\existing.csr"
When using an existing CSR, make sure you already have the matching private key stored securely. Cloudflare will return the certificate, but it will not have your private key.
Choose Validity and Key Type
The script accepts these Origin CA validity values:
7, 30, 90, 365, 730, 1095, 5475
For example, to request the longest supported validity in this script:
$RequestedValidityDays = 5475
Choose the request type:
$RequestType = "origin-rsa"
Supported values in the script are:
origin-rsaorigin-ecc
Optional Cleanup and Revocation Settings
By default, the script keeps existing local files, keeps CSR files, and does not revoke old Cloudflare certificates:
$RemoveOldLocalFiles = $false $RemoveCsrAfterCreate = $false $RevokeOldCloudflareOriginCertificates = $false
Only enable revocation after you understand the effect:
$RevokeOldCloudflareOriginCertificates = $true
Revocation cannot be undone. If the old certificate is still installed on an origin server, that hostname can break in **Full (strict)** mode until the new certificate is installed.
How to Run the Script
Generic command:
.\script_location\scriptname.ps1
Concrete example:
Set-Location C:\Scripts\Cloudflare .\Generate-CloudflareOriginCerts.ps1
If PowerShell blocks the script, see:
Make Sure PowerShell Scripts Can Run on Windows
Install the Certificates on the Origin Server
After the script finishes, copy each .pem certificate and its matching .key file to the correct origin server.
For NGINX, the certificate paths are commonly configured like this:
ssl_certificate /etc/ssl/cloudflare/example.com.pem; ssl_certificate_key /etc/ssl/cloudflare/example.com.key;
Then test and reload NGINX:
sudo nginx -t sudo systemctl reload nginx
After the origin server is using the certificate, set the Cloudflare SSL/TLS encryption mode to **Full (strict)** for the hostname or zone.
Check the Summary CSV
The script writes a CSV file named like this:
Cloudflare-OriginCA-Summary-20260706-010000.csv
Use it as your certificate inventory starting point. It includes the certificate ID and expiration date returned by Cloudflare.
Cloudflare notes that it does not currently send expiration notifications for Origin CA certificates, so track expiration dates in your own monitoring or documentation.
Important Notes
- Use API tokens with least-privilege zone scope whenever possible.
- Do not commit
.key,.csr,.pem, CSV summary, or credential files into a repository. - Store private keys securely and restrict filesystem permissions on origin servers.
- Do not use Cloudflare Origin CA certificates for hostnames that need to work directly without Cloudflare proxying.
- Cloudflare Origin CA certificates cannot use IP addresses as SANs.
- Cloudflare Origin CA certificates can include up to 200 SANs.
The full script is below. Save it as Generate-CloudflareOriginCerts.ps1.
PowerShell Script
Clear-Host
Push-Location $PSScriptRoot
#region Script Settings
$ErrorActionPreference = "Stop"
$Host.UI.RawUI.WindowTitle = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
# Check PowerShell version
if ($PSVersionTable.PSVersion.Major -lt 7) {
Write-Host "This script requires PowerShell 7 or higher. Please upgrade your PowerShell version to continue." -ForegroundColor Red
exit
}
# Continue with script if version is 7 or higher
Write-Host "Running on PowerShell 7 or higher." -ForegroundColor Green
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Valid values:
# ApiToken = Authorization: Bearer <token>
# OriginCAKey = X-Auth-User-Service-Key: <key>
# GlobalApiKey = X-Auth-Email + X-Auth-Key
$CloudflareAuthMode = "GlobalApiKey"
# Required when CloudflareAuthMode = GlobalApiKey
$CloudflareEmail = 'PUT_YOUR_CLOUDFLARE_EMAIL_HERE'
# For ApiToken, put the API Token here.
# For OriginCAKey, put the Origin CA Key here.
# For GlobalApiKey, put the Global API Key here.
$CloudflareApiKey = 'PUT_YOUR_CLOUDFLARE_KEY_HERE'
$CloudflareApiTimeoutSeconds = 120
$CertOutputPath = "C:\DevOps\Certs"
# Valid values:
# LocalOpenSsl = generate private key and CSR locally, then send CSR to Cloudflare
# ExistingCsr = use an existing CSR file from ExistingCsrPath
$CertificateRequestMode = "LocalOpenSsl"
# Only used when CertificateRequestMode = ExistingCsr
$ExistingCsrPath = "C:\DevOps\Certs\existing.csr"
# Valid Cloudflare Origin CA validity values:
# 7, 30, 90, 365, 730, 1095, 5475
$RequestedValidityDays = 5475
# Valid values:
# origin-rsa
# origin-ecc
$RequestType = "origin-rsa"
# $false = keep existing local .pem/.key/.csr files and create timestamped files when needed.
# $true = delete existing local files for the same domain before creating new ones.
$RemoveOldLocalFiles = $false
# $false = keep CSR files after certificate creation.
# $true = remove CSR files after certificate creation.
$RemoveCsrAfterCreate = $false
# $false = do not revoke existing Cloudflare Origin CA certificates.
# $true = revoke existing Cloudflare Origin CA certs where hostnames exactly match domain + wildcard.
$RevokeOldCloudflareOriginCertificates = $false
$Domains = @(
"domain1.com",
"domain2.com",
"domain3.com",
"domain4.com"
)
#endregion Script Settings
#region Functions
function Write-LogMessage {
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[ValidateSet("INFO", "SUCCESS", "WARNING", "ERROR")]
[string]$Level = "INFO"
)
$Prefix = "[{0}] [{1}]" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Level
switch ($Level) {
"SUCCESS" { Write-Host "$Prefix $Message" -ForegroundColor Green }
"WARNING" { Write-Host "$Prefix $Message" -ForegroundColor Yellow }
"ERROR" { Write-Host "$Prefix $Message" -ForegroundColor Red }
default { Write-Host "$Prefix $Message" }
}
}
function Test-RequiredValue {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[string]$Value
)
Write-Host "Checking required value: $Name"
if ([string]::IsNullOrWhiteSpace($Value)) {
throw "$Name is blank."
}
if ($Value -like "PUT_YOUR_*") {
throw "$Name still contains the placeholder value."
}
Write-Host "$Name check completed."
}
function Test-CertificateOutputPath {
Write-Host "Checking certificate output path."
if (!(Test-Path -LiteralPath $CertOutputPath)) {
New-Item -Path $CertOutputPath -ItemType Directory -Force | Out-Null
Write-Host "Created certificate output path: $CertOutputPath"
}
if (!(Test-Path -LiteralPath $CertOutputPath)) {
throw "Certificate output path does not exist and could not be created: $CertOutputPath"
}
Write-Host "Certificate output path check completed: $CertOutputPath"
}
function Get-OpenSslPath {
Write-Host "Checking for OpenSSL."
$OpenSslCommand = Get-Command "openssl.exe" -ErrorAction SilentlyContinue
if ($null -ne $OpenSslCommand) {
Write-Host "OpenSSL found at $($OpenSslCommand.Source)"
return $OpenSslCommand.Source
}
$PossiblePaths = @(
"C:\Program Files\Git\usr\bin\openssl.exe",
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe",
"C:\Program Files\OpenSSL-Win32\bin\openssl.exe"
)
foreach ($PossiblePath in $PossiblePaths) {
if (Test-Path -LiteralPath $PossiblePath) {
Write-Host "OpenSSL found at $PossiblePath"
return $PossiblePath
}
}
throw "OpenSSL was not found. Install OpenSSL or Git for Windows, then rerun this script."
}
function New-CloudflareHeaders {
Write-Host "Creating Cloudflare API headers."
if ($CloudflareAuthMode -eq "ApiToken") {
return @{
"Authorization" = "Bearer $CloudflareApiKey"
"Content-Type" = "application/json"
"Accept" = "application/json"
}
}
if ($CloudflareAuthMode -eq "OriginCAKey") {
return @{
"X-Auth-User-Service-Key" = $CloudflareApiKey
"Content-Type" = "application/json"
"Accept" = "application/json"
}
}
if ($CloudflareAuthMode -eq "GlobalApiKey") {
if ([string]::IsNullOrWhiteSpace($CloudflareEmail)) {
throw "CloudflareEmail is required when CloudflareAuthMode is GlobalApiKey."
}
return @{
"X-Auth-Email" = $CloudflareEmail
"X-Auth-Key" = $CloudflareApiKey
"Content-Type" = "application/json"
"Accept" = "application/json"
}
}
throw "Invalid CloudflareAuthMode. Use ApiToken, OriginCAKey, or GlobalApiKey."
}
function Invoke-CloudflareRequest {
param(
[Parameter(Mandatory = $true)]
[ValidateSet("GET", "POST", "DELETE")]
[string]$Method,
[Parameter(Mandatory = $true)]
[string]$Uri,
[object]$Body = $null
)
Write-Host "Calling Cloudflare API: $Method $Uri"
$Headers = New-CloudflareHeaders
$InvokeParams = @{
Method = $Method
Uri = $Uri
Headers = $Headers
TimeoutSec = $CloudflareApiTimeoutSeconds
}
if ($PSVersionTable.PSVersion.Major -lt 6) {
$InvokeParams.UseBasicParsing = $true
}
if ($null -ne $Body) {
$JsonBody = $Body | ConvertTo-Json -Depth 20
$InvokeParams.Body = $JsonBody
}
Write-Host "Sending request to Cloudflare. Timeout is $CloudflareApiTimeoutSeconds seconds."
try {
$Response = Invoke-RestMethod @InvokeParams
}
catch {
throw "Cloudflare API request failed: $($_.Exception.Message)"
}
if ($null -eq $Response) {
throw "Cloudflare API returned an empty response."
}
if ($Response.success -ne $true) {
$ErrorMessages = @()
if ($Response.errors) {
foreach ($CloudflareError in $Response.errors) {
$ErrorMessages += "Code $($CloudflareError.code): $($CloudflareError.message)"
}
}
if ($ErrorMessages.Count -eq 0) {
$ErrorMessages += "Unknown Cloudflare API error."
}
throw ($ErrorMessages -join " | ")
}
Write-Host "Cloudflare API call completed."
return $Response
}
function Get-SafeFileBaseName {
param(
[Parameter(Mandatory = $true)]
[string]$Domain
)
return ($Domain -replace "[^a-zA-Z0-9\.\-_]", "_").ToLower()
}
function Get-CertificateFilePaths {
param(
[Parameter(Mandatory = $true)]
[string]$Domain
)
$SafeDomain = Get-SafeFileBaseName -Domain $Domain
$BaseKeyPath = Join-Path $CertOutputPath "$SafeDomain.key"
$BasePemPath = Join-Path $CertOutputPath "$SafeDomain.pem"
$BaseCsrPath = Join-Path $CertOutputPath "$SafeDomain.csr"
if ($RemoveOldLocalFiles -eq $true) {
Write-Host "RemoveOldLocalFiles is enabled. Removing existing local files for $Domain."
foreach ($ExistingPath in @($BaseKeyPath, $BasePemPath, $BaseCsrPath)) {
if (Test-Path -LiteralPath $ExistingPath) {
Remove-Item -LiteralPath $ExistingPath -Force
Write-Host "Removed $ExistingPath"
}
}
return @{
KeyPath = $BaseKeyPath
PemPath = $BasePemPath
CsrPath = $BaseCsrPath
}
}
if ((Test-Path -LiteralPath $BaseKeyPath) -or (Test-Path -LiteralPath $BasePemPath) -or (Test-Path -LiteralPath $BaseCsrPath)) {
$TimeStamp = Get-Date -Format "yyyyMMdd-HHmmss"
return @{
KeyPath = Join-Path $CertOutputPath "$SafeDomain.$TimeStamp.key"
PemPath = Join-Path $CertOutputPath "$SafeDomain.$TimeStamp.pem"
CsrPath = Join-Path $CertOutputPath "$SafeDomain.$TimeStamp.csr"
}
}
return @{
KeyPath = $BaseKeyPath
PemPath = $BasePemPath
CsrPath = $BaseCsrPath
}
}
function New-OriginCertificateRequest {
param(
[Parameter(Mandatory = $true)]
[string]$Domain,
[Parameter(Mandatory = $true)]
[string]$OpenSslPath,
[Parameter(Mandatory = $true)]
[string]$KeyPath,
[Parameter(Mandatory = $true)]
[string]$CsrPath
)
Write-Host "Generating private key and CSR for $Domain."
$OpenSslConfigPath = Join-Path $env:TEMP "$($Domain.Replace(".", "_"))_openssl.cnf"
$OpenSslConfig = @"
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[ dn ]
CN = $Domain
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = $Domain
DNS.2 = *.$Domain
"@
Set-Content -LiteralPath $OpenSslConfigPath -Value $OpenSslConfig -Encoding ASCII
if (!(Test-Path -LiteralPath $OpenSslConfigPath)) {
throw "Failed to create OpenSSL config file: $OpenSslConfigPath"
}
Write-Host "OpenSSL config created: $OpenSslConfigPath"
& $OpenSslPath req `
-new `
-newkey rsa:2048 `
-nodes `
-keyout $KeyPath `
-out $CsrPath `
-config $OpenSslConfigPath
if ($LASTEXITCODE -ne 0) {
throw "OpenSSL failed while generating CSR for $Domain."
}
if (!(Test-Path -LiteralPath $KeyPath)) {
throw "Private key was not created: $KeyPath"
}
if (!(Test-Path -LiteralPath $CsrPath)) {
throw "CSR was not created: $CsrPath"
}
Write-Host "Private key created: $KeyPath"
Write-Host "CSR created: $CsrPath"
Remove-Item -LiteralPath $OpenSslConfigPath -Force
Write-Host "Temporary OpenSSL config removed."
}
function Copy-ExistingCertificateRequest {
param(
[Parameter(Mandatory = $true)]
[string]$SourceCsrPath,
[Parameter(Mandatory = $true)]
[string]$TargetCsrPath
)
Write-Host "Using existing CSR: $SourceCsrPath"
if (!(Test-Path -LiteralPath $SourceCsrPath)) {
throw "Existing CSR was not found: $SourceCsrPath"
}
Copy-Item -LiteralPath $SourceCsrPath -Destination $TargetCsrPath -Force
if (!(Test-Path -LiteralPath $TargetCsrPath)) {
throw "Failed to copy CSR to: $TargetCsrPath"
}
Write-Host "CSR copied to: $TargetCsrPath"
}
function New-CloudflareOriginCertificate {
param(
[Parameter(Mandatory = $true)]
[string]$Domain,
[Parameter(Mandatory = $true)]
[string]$CsrPath,
[Parameter(Mandatory = $true)]
[string]$PemPath
)
Write-Host "Reading CSR for $Domain."
$CsrText = Get-Content -LiteralPath $CsrPath -Raw
if ([string]::IsNullOrWhiteSpace($CsrText)) {
throw "CSR file is empty: $CsrPath"
}
$HostNames = @(
$Domain,
"*.$Domain"
)
$Body = @{
csr = $CsrText
hostnames = $HostNames
request_type = $RequestType
requested_validity = $RequestedValidityDays
}
$Response = Invoke-CloudflareRequest `
-Method "POST" `
-Uri "https://api.cloudflare.com/client/v4/certificates" `
-Body $Body
if ([string]::IsNullOrWhiteSpace($Response.result.certificate)) {
throw "Cloudflare did not return a certificate for $Domain."
}
Set-Content -LiteralPath $PemPath -Value $Response.result.certificate -Encoding ASCII
if (!(Test-Path -LiteralPath $PemPath)) {
throw "PEM file was not created: $PemPath"
}
Write-Host "Origin certificate created: $PemPath"
Write-Host "Cloudflare Certificate ID: $($Response.result.id)"
Write-Host "Expires On: $($Response.result.expires_on)"
return $Response.result
}
function Get-CloudflareZone {
param(
[Parameter(Mandatory = $true)]
[string]$Domain
)
Write-Host "Looking up Cloudflare zone for $Domain."
$EncodedDomain = [System.Uri]::EscapeDataString($Domain)
$Uri = "https://api.cloudflare.com/client/v4/zones?name=$EncodedDomain"
$Response = Invoke-CloudflareRequest -Method "GET" -Uri $Uri
if ($null -eq $Response.result -or $Response.result.Count -eq 0) {
throw "No Cloudflare zone found for $Domain."
}
$Zone = $Response.result | Select-Object -First 1
if ([string]::IsNullOrWhiteSpace($Zone.id)) {
throw "Cloudflare zone lookup did not return a zone ID for $Domain."
}
Write-Host "Cloudflare zone found for $Domain`: $($Zone.id)"
return $Zone
}
function Get-CloudflareOriginCertificates {
param(
[Parameter(Mandatory = $true)]
[string]$ZoneId
)
Write-Host "Listing existing Cloudflare Origin CA certificates for zone $ZoneId."
$AllCertificates = @()
$Page = 1
$PerPage = 50
do {
$Uri = "https://api.cloudflare.com/client/v4/certificates?zone_id=$ZoneId&page=$Page&per_page=$PerPage"
$Response = Invoke-CloudflareRequest -Method "GET" -Uri $Uri
if ($Response.result) {
$AllCertificates += $Response.result
}
$TotalPages = 1
if ($Response.result_info -and $Response.result_info.total_pages) {
$TotalPages = [int]$Response.result_info.total_pages
}
$Page++
}
while ($Page -le $TotalPages)
Write-Host "Found $($AllCertificates.Count) existing Origin CA certificate(s)."
return $AllCertificates
}
function Remove-CloudflareOriginCertificatesForDomain {
param(
[Parameter(Mandatory = $true)]
[string]$Domain
)
if ($RevokeOldCloudflareOriginCertificates -ne $true) {
Write-Host "Cloudflare revoke switch is disabled. Existing Cloudflare Origin CA certificates will be kept."
return
}
if ($CloudflareAuthMode -notin @("ApiToken", "GlobalApiKey")) {
throw "RevokeOldCloudflareOriginCertificates requires ApiToken or GlobalApiKey because the script must look up the zone."
}
Write-Host "RevokeOldCloudflareOriginCertificates is enabled for $Domain."
$Zone = Get-CloudflareZone -Domain $Domain
$Certificates = Get-CloudflareOriginCertificates -ZoneId $Zone.id
$ExpectedHostNames = @(
$Domain.ToLower(),
"*.$Domain".ToLower()
)
$MatchingCertificates = @()
foreach ($Certificate in $Certificates) {
if ($null -eq $Certificate.hostnames) {
continue
}
$CertHostNames = @($Certificate.hostnames | ForEach-Object { $_.ToLower() } | Sort-Object)
$ExpectedSorted = @($ExpectedHostNames | Sort-Object)
$CertHostNamesJoined = $CertHostNames -join "|"
$ExpectedJoined = $ExpectedSorted -join "|"
if ($CertHostNamesJoined -eq $ExpectedJoined) {
$MatchingCertificates += $Certificate
}
}
if ($MatchingCertificates.Count -eq 0) {
Write-Host "No matching existing Cloudflare Origin CA certificates found for $Domain."
return
}
foreach ($Certificate in $MatchingCertificates) {
if ([string]::IsNullOrWhiteSpace($Certificate.id)) {
Write-LogMessage -Level "WARNING" -Message "Skipping certificate with missing ID for $Domain."
continue
}
Write-Host "Revoking existing Cloudflare Origin CA certificate $($Certificate.id) for $Domain."
$RevokeUri = "https://api.cloudflare.com/client/v4/certificates/$($Certificate.id)"
Invoke-CloudflareRequest -Method "DELETE" -Uri $RevokeUri | Out-Null
Write-Host "Revoked Cloudflare Origin CA certificate $($Certificate.id)."
}
}
function Remove-CertificateRequestFile {
param(
[Parameter(Mandatory = $true)]
[string]$CsrPath
)
if ($RemoveCsrAfterCreate -ne $true) {
Write-Host "RemoveCsrAfterCreate is disabled. Keeping CSR file: $CsrPath"
return
}
Write-Host "RemoveCsrAfterCreate is enabled. Removing CSR file: $CsrPath"
if (Test-Path -LiteralPath $CsrPath) {
Remove-Item -LiteralPath $CsrPath -Force
Write-Host "Removed CSR file: $CsrPath"
}
if (Test-Path -LiteralPath $CsrPath) {
throw "CSR file still exists after removal attempt: $CsrPath"
}
}
#endregion Functions
#region Main
try {
Write-LogMessage -Message "Starting Cloudflare Origin CA certificate generation."
Test-RequiredValue -Name "CloudflareAuthMode" -Value $CloudflareAuthMode
Test-RequiredValue -Name "CloudflareApiKey" -Value $CloudflareApiKey
if ($CloudflareAuthMode -eq "GlobalApiKey") {
Test-RequiredValue -Name "CloudflareEmail" -Value $CloudflareEmail
}
Test-RequiredValue -Name "CertOutputPath" -Value $CertOutputPath
Test-RequiredValue -Name "CertificateRequestMode" -Value $CertificateRequestMode
if ($RequestedValidityDays -notin @(7, 30, 90, 365, 730, 1095, 5475)) {
throw "RequestedValidityDays must be one of: 7, 30, 90, 365, 730, 1095, 5475."
}
Write-Host "RequestedValidityDays check completed."
if ($RequestType -notin @("origin-rsa", "origin-ecc")) {
throw "RequestType must be origin-rsa or origin-ecc."
}
Write-Host "RequestType check completed."
if ($CertificateRequestMode -notin @("LocalOpenSsl", "ExistingCsr")) {
throw "CertificateRequestMode must be LocalOpenSsl or ExistingCsr."
}
Write-Host "CertificateRequestMode check completed."
Test-CertificateOutputPath
$OpenSslPath = $null
if ($CertificateRequestMode -eq "LocalOpenSsl") {
$OpenSslPath = Get-OpenSslPath
}
if ($CertificateRequestMode -eq "ExistingCsr") {
Test-RequiredValue -Name "ExistingCsrPath" -Value $ExistingCsrPath
}
$Results = @()
foreach ($Domain in $Domains) {
Write-Host ""
Write-LogMessage -Message "Processing $Domain"
if ([string]::IsNullOrWhiteSpace($Domain)) {
Write-LogMessage -Level "WARNING" -Message "Skipping blank domain entry."
continue
}
$Domain = $Domain.Trim().ToLower()
Remove-CloudflareOriginCertificatesForDomain -Domain $Domain
$Paths = Get-CertificateFilePaths -Domain $Domain
if ($CertificateRequestMode -eq "LocalOpenSsl") {
New-OriginCertificateRequest `
-Domain $Domain `
-OpenSslPath $OpenSslPath `
-KeyPath $Paths.KeyPath `
-CsrPath $Paths.CsrPath
}
if ($CertificateRequestMode -eq "ExistingCsr") {
Copy-ExistingCertificateRequest `
-SourceCsrPath $ExistingCsrPath `
-TargetCsrPath $Paths.CsrPath
$Paths.KeyPath = ""
}
$CertificateResult = New-CloudflareOriginCertificate `
-Domain $Domain `
-CsrPath $Paths.CsrPath `
-PemPath $Paths.PemPath
Remove-CertificateRequestFile -CsrPath $Paths.CsrPath
$Results += [PSCustomObject]@{
Domain = $Domain
HostNames = "$Domain, *.$Domain"
KeyPath = $Paths.KeyPath
PemPath = $Paths.PemPath
CsrPath = $Paths.CsrPath
CertificateId = $CertificateResult.id
ExpiresOn = $CertificateResult.expires_on
RequestType = $CertificateResult.request_type
ValidityDays = $CertificateResult.requested_validity
RequestMode = $CertificateRequestMode
}
Write-LogMessage -Level "SUCCESS" -Message "Completed $Domain"
}
$SummaryPath = Join-Path $CertOutputPath ("Cloudflare-OriginCA-Summary-{0}.csv" -f (Get-Date -Format "yyyyMMdd-HHmmss"))
$Results | Export-Csv -LiteralPath $SummaryPath -NoTypeInformation -Encoding UTF8
if (!(Test-Path -LiteralPath $SummaryPath)) {
throw "Summary CSV was not created: $SummaryPath"
}
Write-Host ""
Write-LogMessage -Level "SUCCESS" -Message "All requested domains processed."
Write-Host "Summary CSV: $SummaryPath"
}
catch {
Write-LogMessage -Level "ERROR" -Message $_.Exception.Message
throw
}
#endregion Main