Steve Borba

My notes, I hope they help you, feel free to comment/add to them

Certificate Automation

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:

  1. Get a new certificate.
  2. Put it where it belongs.
  3. Get rid of the old one.
  4. Delete the temporary PFX. (optional, or delete the whole folder)
  5. 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"
}
main
Cloudflare
# ===================== 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"
}
main
Route 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

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>