0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
JPPINTO
  • Home
  • Blog
  • Certifications
  • About
  • Contact
  • Shop
  • Gallery
  • Current Setup
Contact

Search

July 6, 2026 / Cloudflare, PowerShell, SSL

Bulk Create Cloudflare Origin CA Certificates with PowerShell

Tags: automation, cloudflare, origin ca, powershell, ssl certificates
Featured image for 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 .pem file.
  • 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-rsa
  • origin-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

References

  • Cloudflare Origin CA documentation
  • Cloudflare API calls and bearer tokens
  • Cloudflare Origin CA keys deprecation
Post Views: 49
<- Test a Cloudflare Global API Key Connection with PowerShell
Turning Our Atlanta Vacation Photos into a Video with PowerShell ->

Categories

  • Active Directory (5)
  • AI (2)
  • Amazon Cloud Services (1)
  • AWS (2)
  • Blazor (1)
  • C# (C-Sharp) (3)
  • CI/CD Pipelines (1)
  • Cloud (1)
  • Cloudflare (2)
  • Containers (4)
  • Deployment (2)
  • Development (4)
  • Docker (3)
  • Family (1)
  • General (5)
  • IIS 6.0 (4)
  • IIS 7.0 (10)
  • IIS 8.0 (1)
  • Infrastructure as Code (IaC) (1)
  • Kubernetes (3)
  • Linux (9)
  • Microsoft 365 (2)
  • MySQL (1)
  • Office 2010 (1)
  • PHP (1)
  • PowerShell (11)
  • Productivity (1)
  • Servers (9)
  • SharePoint 2007 (8)
  • SharePoint 2010 (19)
  • SharePoint 2013 (2)
  • SharePoint Online (1)
  • SMTP (4)
  • SQL Server 2008 (1)
  • SQL Server 2008 R2 (1)
  • SQL Server 2012 (2)
  • SQL Server 2019 (1)
  • SSL (1)
  • Travel (1)
  • Troubleshooting (1)
  • Ubuntu (9)
  • Uncategorized (1)
  • URL Rewrite (2)
  • Visual Studio 2019 (1)
  • Visual Studio Code (1)
  • Windows 10 (7)
  • Windows 2003 (9)
  • Windows 2008 (18)
  • Windows 2012 (6)
  • Windows 7 (3)
  • Windows Firewall (1)
  • Windows Vista (1)
  • WordPress (3)
  • WP-CLI (3)

Recent Posts

  • Turning Our Atlanta Vacation Photos into a Video with PowerShell
  • Bulk Create Cloudflare Origin CA Certificates with PowerShell
  • Test a Cloudflare Global API Key Connection with PowerShell
  • Entering .com in Bucket Names Causes SSL Errors
  • Transfer S3 Contents Between Buckets with PowerShell

Advertisement

Tags

ai coding agents aws bash cloudflare cloud storage developer workflow dev to production externalize blob externalize sharepoint data filezilla server full installation http redirect https IIS iis7 iis 7 installation IIS installation index server configuration installing cumulative updates linux load balance central administration microsoft 365 nginx powerpoint powershell redirect http to https s3 server setup sharepoint 2010 cumulative updates sharepoint 2010 farm build sharepoint 2010 farm configuration sharepoint 2010 farm installation sharepoint data externalization SMTP ssl storagepoint ubuntu web server windows Windows 7 windows firewall configuration windows server 2008 wordpress wp-cli x86
© 2026 JPPinto.com. All rights reserved.