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
#Requires -Version 7
param([switch]$Update)
# ===================== Variables =====================
$ConfigPath = Join-Path $PSScriptRoot 'acme-config.xml'
function Read-Value {
param([string]$Prompt, [string]$Default)
$suffix = if ($Default) { " [$Default]" } else { '' }
$v = Read-Host "$Prompt$suffix"
if ([string]::IsNullOrWhiteSpace($v)) { $Default } else { $v.Trim() }
}
function Read-Secret {
param([string]$Prompt, [securestring]$Default)
$hint = if ($Default) { ' [Enter = keep existing]' } else { ' [Enter = skip]' }
$v = Read-Host "$Prompt$hint" -AsSecureString
if ($v.Length -eq 0) { $Default } else { $v }
}
function ConvertFrom-Secure {
param([securestring]$Secure)
[System.Net.NetworkCredential]::new('', $Secure).Password
}
if ($Update -or -not (Test-Path $ConfigPath)) {
$old = if (Test-Path $ConfigPath) { Import-Clixml $ConfigPath } else { @{} }
$PANMGMTIP = Read-Value 'Panorama mgmt IP' $old.PANMGMTIP
$PANUser = Read-Value 'Panorama admin user (blank = keep existing API key)'
$PANKey = if ($PANUser) {
$PANPass = Read-Host 'Panorama admin password' -AsSecureString
$uri = "https://$PANMGMTIP/api/?type=keygen&user=$([uri]::EscapeDataString($PANUser))&password=$([uri]::EscapeDataString((ConvertFrom-Secure $PANPass)))"
ConvertTo-SecureString (Invoke-RestMethod -Uri $uri -SkipCertificateCheck).response.result.key -AsPlainText -Force
} else { $old.PANKey }
$cfg = @{
AZSubscriptionId = Read-Value 'Azure subscription ID' $old.AZSubscriptionId
AZTenantId = Read-Value 'Azure tenant ID' $old.AZTenantId
AZAppClientId = Read-Value 'Azure app client ID' $old.AZAppClientId
AZAppClientSecret = Read-Secret 'Azure app client secret' $old.AZAppClientSecret
AcmeServer = Read-Value 'ACME server (LE_STAGE, LE_PROD, or directory URL)' $(if ($old.AcmeServer) { $old.AcmeServer } else { 'LE_STAGE' })
EabKeyId = Read-Value 'EAB key ID (blank for Let''s Encrypt)' $old.EabKeyId
EabHmacKey = Read-Secret 'EAB HMAC key (blank for Let''s Encrypt)' $old.EabHmacKey
Contact = Read-Value 'Contact email' $old.Contact
PANMGMTIP = $PANMGMTIP
PANKey = $PANKey
Domains = @()
}
Write-Host "`nDomains (blank name to finish)"
$i = 0
while ($true) {
$o = if ($i -lt $old.Domains.Count) { $old.Domains[$i] } else { @{} }
$name = Read-Value "Domain #$($i+1) name" $o.Name
if (-not $name) { break }
$cfg.Domains += @{
Name = $name
DnsAlias = Read-Value ' DNS alias (FQDN)' $o.DnsAlias
Template = Read-Value ' Panorama template' $o.Template
PushStack = Read-Value ' Template stack to push' $o.PushStack
}
$i++
}
$cfg | Export-Clixml -Path $ConfigPath
Write-Host "Saved to $ConfigPath"
if ($Update) { return }
}
$cfg = Import-Clixml $ConfigPath
$AZSubscriptionId = $cfg.AZSubscriptionId
$AZTenantId = $cfg.AZTenantId
$AZAppClientId = $cfg.AZAppClientId
$AZAppClientSecret = $cfg.AZAppClientSecret
$AcmeServer = $cfg.AcmeServer
$EabKeyId = $cfg.EabKeyId
$EabHmacKey = if ($cfg.EabHmacKey) { ConvertFrom-Secure $cfg.EabHmacKey }
$Contact = $cfg.Contact
$PANMGMTIP = $cfg.PANMGMTIP
$PANKey = ConvertFrom-Secure $cfg.PANKey
$Domains = $cfg.Domains
# ===================== Variables =====================
$PfxPass = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object { [char]$_ })
$AzureArgs = @{
AZSubscriptionId = $AZSubscriptionId
AZTenantId = $AZTenantId
AZAppCred = [pscredential]::new($AZAppClientId, $AZAppClientSecret)
}
function main {
if (-not (Get-Module -ListAvailable -Name Posh-ACME)) {
Install-Module -Name Posh-ACME -Scope CurrentUser -Force -ErrorAction Stop
}
Import-Module Posh-ACME
Set-PAServer $AcmeServer
if (-not (Get-PAAccount)) {
$acct = @{ Contact = $Contact; AcceptTOS = $true }
if ($EabKeyId) { $acct += @{ EabKeyId = $EabKeyId; EabHmacKey = $EabHmacKey } }
New-PAAccount @acct
}
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
}
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))"
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"
}
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