WHAT!? 47 day certificate!!! oh man! that’s crazy
I know we aren’t there yet, but if we are going to get there I am going to build the system like we are – and treat the current cert length as a safety net as we get this system rolling.
Once we’re actually dealing with 47-day certificates, it can’t be someone’s job to update them any more. We need that on a cron job and monitoring to start yelling if the cert is older than 32 days. I also don’t want to create a giant PKI system to manage – I want to treat these certificates as a consumable item. Now they will be more like gas, not like changing the oil.
The basic process I’m working toward is:
- Get a new certificate.
- Put it where it belongs.
- Get rid of the old one.
- Delete the temporary PFX. (optional, or delete the whole folder)
- Move on.
Azure using cname delegation
# ===================== Variables =====================
# Azure DNS API creds (for writing ACME TXT records via CNAME delegation)
$AZSubscriptionId = '<AZ_SUB_ID>'
$AZTenantId = '<AZ_TENANT_ID>'
$AZAppClientId = '<AZ_APP_CLIENT_ID>'
$AZAppClientSecret = '<AZ_APP_CLIENT_SECRET>'
# Sectigo ACME
$SectigoServer = '<SECTIGO_ACME_DIRECTORY_URL>'
$EabKeyId = '<SECTIGO_EAB_KEY_ID>'
$EabHmacKey = '<SECTIGO_EAB_HMAC_KEY>'
$Contact = '<CONTACT_EMAIL>'
# Panorama
$PANMGMTIP = '<PANORAMA_MGMT_IP>'
$PANKey = '<PAN_API_KEY>'
# Per-domain config
$Domains = @(
@{ Name = '<gp.contoso.com>'; DnsAlias = '<cname-target-in-azure-zone>'; Template = '<ONSITE>'; PushStack = '<ONSITE_Stack>' },
@{ Name = '<prisma.contoso.com>'; DnsAlias = '<cname-target-in-azure-zone>'; Template = '<Mobile_User>'; PushStack = '<Mobile_User_Stack>' }
)
# ===================== Variables =====================
$PfxPass = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object { [char]$_ })
$AzureArgs = @{
AZSubscriptionId = $AZSubscriptionId
AZTenantId = $AZTenantId
AZAppClientId = $AZAppClientId
AZAppClientSecret = (ConvertTo-SecureString $AZAppClientSecret -AsPlainText -Force)
}
function main {
Import-Module Posh-ACME
Set-PAServer $SectigoServer
if (-not (Get-PAAccount)) {
New-PAAccount -Contact $Contact -EabKeyId $EabKeyId -EabHmacKey $EabHmacKey -AcceptTOS
}
# ===================== Issue + Upload =====================
foreach ($d in $Domains) {
Write-Host "Requesting cert for $($d.Name)..."
$CertOrder = New-PACertificate -Domain $d.Name -Plugin Azure -PluginArgs $AzureArgs -DnsAlias $d.DnsAlias -PfxPass $PfxPass -Force
$FullChain = Join-Path $CertOrder.CertFolder 'fullchain.cer'
$PfxPath = Join-Path $CertOrder.CertFolder 'cert.pfx'
Write-Host "Uploading cert for $($d.Name) to template $($d.Template)..."
$certUri = "https://$PANMGMTIP/api/?type=import&category=certificate&certificate-name=$($d.Name)&format=pem&target-tpl=$($d.Template)&key=$PANKey"
Invoke-RestMethod -Uri $certUri -Method Post -Form @{ file = Get-Item $FullChain } -SkipCertificateCheck
$keyUri = "https://$PANMGMTIP/api/?type=import&category=private-key&certificate-name=$($d.Name)&format=pkcs12&passphrase=$PfxPass&target-tpl=$($d.Template)&key=$PANKey"
Invoke-RestMethod -Uri $keyUri -Method Post -Form @{ file = Get-Item $PfxPath } -SkipCertificateCheck
Remove-Item -Path $PfxPath -Force
# Remove-Item -Path $CertOrder.CertFolder -Recurse -Force
}
# ===================== Commit / Push =====================
Write-Host "Committing changes on Panorama..."
$commitUri = "https://$PANMGMTIP/api/?type=commit&cmd=<commit></commit>&key=$PANKey"
$commitResp = Invoke-RestMethod -Uri $commitUri -Method Post -SkipCertificateCheck
Wait-PanJob -JobId $commitResp.response.result.job -PanIp $PANMGMTIP -ApiKey $PANKey
foreach ($d in $Domains) {
Write-Host "Pushing to template stack $($d.PushStack) for $($d.Name)..."
$cmd = "<commit-all><template-stack><entry name='$($d.PushStack)'/></template-stack></commit-all>"
$pushUri = "https://$PANMGMTIP/api/?type=commit&action=all&key=$PANKey&cmd=$([uri]::EscapeDataString($cmd))"
$pushResp = Invoke-RestMethod -Uri $pushUri -Method Post -SkipCertificateCheck
}
}
function Wait-PanJob {
param(
[Parameter(Mandatory)] [string]$JobId,
[Parameter(Mandatory)] [string]$PanIp,
[Parameter(Mandatory)] [string]$ApiKey,
[int]$PollSeconds = 5
)
do {
Start-Sleep -Seconds $PollSeconds
$statusUri = "https://$PanIp/api/?type=op&cmd=<show><jobs><id>$JobId</id></jobs></show>&key=$ApiKey"
$statusResp = Invoke-RestMethod -Uri $statusUri -Method Get -SkipCertificateCheck
$status = $statusResp.response.result.job.status
Write-Host " Job $JobId status: $status"
} while ($status -ne 'FIN')
$result = $statusResp.response.result.job.result
if ($result -ne 'OK') {
throw "Job $JobId finished with result: $result"
}
Write-Host " Job $JobId completed OK"
}
mainCloudflare
# ===================== Variables =====================
# Cloudflare API token (Zone.DNS Edit permission on the relevant zone)
$CFApiToken = '<CLOUDFLARE_API_TOKEN>'
# Sectigo ACME
$SectigoServer = '<SECTIGO_ACME_DIRECTORY_URL>'
$EabKeyId = '<SECTIGO_EAB_KEY_ID>'
$EabHmacKey = '<SECTIGO_EAB_HMAC_KEY>'
$Contact = '<CONTACT_EMAIL>'
# Panorama
$PANMGMTIP = '<PANORAMA_MGMT_IP>'
$PANKey = '<PAN_API_KEY>'
# Per-domain config
$Domains = @(
@{ Name = '<gp.contoso.com>'; Template = '<ONSITE>'; PushStack = '<ONSITE_Stack>' },
@{ Name = '<prisma.contoso.com>'; Template = '<Mobile_User>'; PushStack = '<Mobile_User_Stack>' }
)
# ===================== Variables =====================
$PfxPass = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object { [char]$_ })
$CFArgs = @{
CFToken = (ConvertTo-SecureString $CFApiToken -AsPlainText -Force)
}
function main {
Import-Module Posh-ACME
Set-PAServer $SectigoServer
if (-not (Get-PAAccount)) {
New-PAAccount -Contact $Contact -EabKeyId $EabKeyId -EabHmacKey $EabHmacKey -AcceptTOS
}
# ===================== Issue + Upload =====================
foreach ($d in $Domains) {
Write-Host "Requesting cert for $($d.Name)..."
$CertOrder = New-PACertificate -Domain $d.Name -Plugin Cloudflare -PluginArgs $CFArgs -PfxPass $PfxPass -Force
$FullChain = Join-Path $CertOrder.CertFolder 'fullchain.cer'
$PfxPath = Join-Path $CertOrder.CertFolder 'cert.pfx'
Write-Host "Uploading cert for $($d.Name) to template $($d.Template)..."
$certUri = "https://$PANMGMTIP/api/?type=import&category=certificate&certificate-name=$($d.Name)&format=pem&target-tpl=$($d.Template)&key=$PANKey"
Invoke-RestMethod -Uri $certUri -Method Post -Form @{ file = Get-Item $FullChain } -SkipCertificateCheck
$keyUri = "https://$PANMGMTIP/api/?type=import&category=private-key&certificate-name=$($d.Name)&format=pkcs12&passphrase=$PfxPass&target-tpl=$($d.Template)&key=$PANKey"
Invoke-RestMethod -Uri $keyUri -Method Post -Form @{ file = Get-Item $PfxPath } -SkipCertificateCheck
Remove-Item -Path $PfxPath -Force
# Remove-Item -Path $CertOrder.CertFolder -Recurse -Force
}
# ===================== Commit / Push =====================
Write-Host "Committing changes on Panorama..."
$commitUri = "https://$PANMGMTIP/api/?type=commit&cmd=<commit></commit>&key=$PANKey"
$commitResp = Invoke-RestMethod -Uri $commitUri -Method Post -SkipCertificateCheck
Wait-PanJob -JobId $commitResp.response.result.job -PanIp $PANMGMTIP -ApiKey $PANKey
foreach ($d in $Domains) {
Write-Host "Pushing to template stack $($d.PushStack) for $($d.Name)..."
$cmd = "<commit-all><template-stack><entry name='$($d.PushStack)'/></template-stack></commit-all>"
$pushUri = "https://$PANMGMTIP/api/?type=commit&action=all&key=$PANKey&cmd=$([uri]::EscapeDataString($cmd))"
$pushResp = Invoke-RestMethod -Uri $pushUri -Method Post -SkipCertificateCheck
}
}
function Wait-PanJob {
param(
[Parameter(Mandatory)] [string]$JobId,
[Parameter(Mandatory)] [string]$PanIp,
[Parameter(Mandatory)] [string]$ApiKey,
[int]$PollSeconds = 5
)
do {
Start-Sleep -Seconds $PollSeconds
$statusUri = "https://$PanIp/api/?type=op&cmd=<show><jobs><id>$JobId</id></jobs></show>&key=$ApiKey"
$statusResp = Invoke-RestMethod -Uri $statusUri -Method Get -SkipCertificateCheck
$status = $statusResp.response.result.job.status
Write-Host " Job $JobId status: $status"
} while ($status -ne 'FIN')
$result = $statusResp.response.result.job.result
if ($result -ne 'OK') {
throw "Job $JobId finished with result: $result"
}
Write-Host " Job $JobId completed OK"
}
mainRoute 53
# ===================== Variables =====================
# AWS Route53 API creds (needs route53:ChangeResourceRecordSets, route53:GetChange, route53:ListHostedZones on the zone)
$R53AccessKey = '<AWS_ACCESS_KEY>'
$R53SecretKey = '<AWS_SECRET_KEY>'
# Sectigo ACME
$SectigoServer = '<SECTIGO_ACME_DIRECTORY_URL>'
$EabKeyId = '<SECTIGO_EAB_KEY_ID>'
$EabHmacKey = '<SECTIGO_EAB_HMAC_KEY>'
$Contact = '<CONTACT_EMAIL>'
# Panorama
$PANMGMTIP = '<PANORAMA_MGMT_IP>'
$PANKey = '<PAN_API_KEY>'
# Per-domain config
$Domains = @(
@{ Name = '<gp.contoso.com>'; Template = '<ONSITE>'; PushStack = '<ONSITE_Stack>' },
@{ Name = '<prisma.contoso.com>'; Template = '<Mobile_User>'; PushStack = '<Mobile_User_Stack>' }
# ===================== Variables =====================
$PfxPass = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object { [char]$_ })
$R53Args = @{
R53AccessKey = $R53AccessKey
R53SecretKey = (ConvertTo-SecureString $R53SecretKey -AsPlainText -Force)
}
#$R53Args = @{R53UseIAMRole = $true}
function main {
Import-Module Posh-ACME
Set-PAServer $SectigoServer
if (-not (Get-PAAccount)) {
New-PAAccount -Contact $Contact -EabKeyId $EabKeyId -EabHmacKey $EabHmacKey -AcceptTOS
}
# ===================== Issue + Upload =====================
foreach ($d in $Domains) {
Write-Host "Requesting cert for $($d.Name)..."
$CertOrder = New-PACertificate -Domain $d.Name -Plugin Route53 -PluginArgs $R53Args -PfxPass $PfxPass -Force
$FullChain = Join-Path $CertOrder.CertFolder 'fullchain.cer'
$PfxPath = Join-Path $CertOrder.CertFolder 'cert.pfx'
Write-Host "Uploading cert for $($d.Name) to template $($d.Template)..."
$certUri = "https://$PANMGMTIP/api/?type=import&category=certificate&certificate-name=$($d.Name)&format=pem&target-tpl=$($d.Template)&key=$PANKey"
Invoke-RestMethod -Uri $certUri -Method Post -Form @{ file = Get-Item $FullChain } -SkipCertificateCheck
$keyUri = "https://$PANMGMTIP/api/?type=import&category=private-key&certificate-name=$($d.Name)&format=pkcs12&passphrase=$PfxPass&target-tpl=$($d.Template)&key=$PANKey"
Invoke-RestMethod -Uri $keyUri -Method Post -Form @{ file = Get-Item $PfxPath } -SkipCertificateCheck
Remove-Item -Path $PfxPath -Force
# Remove-Item -Path $CertOrder.CertFolder -Recurse -Force
}
# ===================== Commit / Push =====================
Write-Host "Committing changes on Panorama..."
$commitUri = "https://$PANMGMTIP/api/?type=commit&cmd= &key=$PANKey"
$commitResp = Invoke-RestMethod -Uri $commitUri -Method Post -SkipCertificateCheck
Wait-PanJob -JobId $commitResp.response.result.job -PanIp $PANMGMTIP -ApiKey $PANKey
foreach ($d in $Domains) {
Write-Host "Pushing to template stack $($d.PushStack) for $($d.Name)..."
$cmd = " "
$pushUri = "https://$PANMGMTIP/api/?type=commit&action=all&key=$PANKey&cmd=$([uri]::EscapeDataString($cmd))"
$pushResp = Invoke-RestMethod -Uri $pushUri -Method Post -SkipCertificateCheck
}
}
function Wait-PanJob {
param(
[Parameter(Mandatory)] [string]$JobId,
[Parameter(Mandatory)] [string]$PanIp,
[Parameter(Mandatory)] [string]$ApiKey,
[int]$PollSeconds = 5
)
do {
Start-Sleep -Seconds $PollSeconds
$statusUri = "https://$PanIp/api/?type=op&cmd=$JobId &key=$ApiKey"
$statusResp = Invoke-RestMethod -Uri $statusUri -Method Get -SkipCertificateCheck
$status = $statusResp.response.result.job.status
Write-Host " Job $JobId status: $status"
} while ($status -ne 'FIN')
$result = $statusResp.response.result.job.result
if ($result -ne 'OK') {
throw "Job $JobId finished with result: $result"
}
Write-Host " Job $JobId completed OK"
}
main