public/Request-AxisCertificate.ps1
|
<# .SYNOPSIS Requests, installs and (on renewal) replaces a CA-issued HTTPS certificate on an Axis device. .DESCRIPTION End-to-end certificate lifecycle for one device, with the private key generated and kept on the camera: 1. Looks for an existing AxisPowershell-managed certificate on the device. If it was issued by a CA and is not within the renewal window, nothing is done (Action = Current) unless -Force is given. 2. Builds subject and SAN from the certificate template (Get-AxisCertificateSubject). 3. Creates a key and CSR on the camera (New-AxisCertificateRequest) using the strongest supported key type. 4. Submits the CSR to the certificate authority (WindowsCA, Dogtag, EST or Command). 5. Installs the certificate and the CA chain, binds it to the web server and optionally enforces HTTPS-only. 6. On renewal: once the new certificate is verified on port 443, the old certificate is removed from the device and revoked on the CA with reason Superseded (failures to revoke are warnings; the new certificate is live). Renewals use a new alias (<Alias>-<timestamp>) so the camera never loses its web server certificate mid-way. Firmware 11.6+ uses the JSON certificate API (with SAN); older firmware falls back to SOAP without SAN. .PARAMETER Device The IP address or hostname of the Axis device. .PARAMETER CertificateAuthority Name of a configured authority (New-AxisCertificateAuthority). Defaults to the template's DefaultAuthority. .PARAMETER Alias Base alias for the certificate on the camera. Defaults to the template's Alias (AxisPowershell). .PARAMETER RenewWithinDays Renew when the current certificate expires within this many days. Defaults to the template value. .PARAMETER Force Request a new certificate even if the current one is valid. .PARAMETER NoHttpsOnly Do not switch the device to HTTPS-only even if the template says so. .PARAMETER NoRevoke Keep the superseded certificate valid on the CA. .EXAMPLE Request-AxisCertificate -Device 192.168.1.100 .EXAMPLE Request-AxisCertificate -Device cam-lobby.example.com -CertificateAuthority CorpCA -Force #> function Request-AxisCertificate { [cmdletbinding()] Param( [Parameter(Mandatory)] [String]$Device, [Parameter()] [String]$CertificateAuthority, [Parameter()] [String]$Alias, [Parameter()] [Int]$RenewWithinDays, [Parameter()] [Switch]$Force, [Parameter()] [Switch]$NoHttpsOnly, [Parameter()] [Switch]$NoRevoke ) Check-Credential $Authority = Resolve-AxisCertificateAuthority -Name $CertificateAuthority if(!$Alias) { $Alias = $Config.Certificate.Alias } if(!$RenewWithinDays) { $RenewWithinDays = $Config.Certificate.RenewWithinDays } $HttpsOnly = $Config.Certificate.HttpsOnly -and !$NoHttpsOnly $Result = [pscustomobject]@{ Device = $Device Action = "Failed" Alias = $Alias Subject = $null Serial = $null NotAfter = $null Thumbprint = $null HttpsVerified = $false RevokedSerial = $null Authority = $Authority.Name Message = "" } ########################################## ## Step 1: Inspect existing certificate ## ########################################## Write-Verbose "$($Device): Checking existing certificates" $Existing = $null Try { $Managed = Get-AxisCertificate -Device $Device | Where-Object { $_.Alias -eq $Alias -or $_.Alias -like "$Alias-*" } #The newest CA-issued one is the one we manage; self-signed placeholders (Issuer = Subject) do not count $Existing = $Managed | Where-Object { $_.Issuer -ne $_.Subject } | Sort-Object NotAfter -Descending | Select-Object -First 1 } Catch { Throw "$($Device): Unable to list certificates: $($_.Exception.Message)" } if($Existing) { $Result.Alias = $Existing.Alias $Result.Subject = $Existing.Subject $Result.Serial = $Existing.Serial $Result.NotAfter = $Existing.NotAfter $Result.Thumbprint = $Existing.Thumbprint $DaysLeft = [Math]::Floor(($Existing.NotAfter - (Get-Date)).TotalDays) if(!$Force -and $DaysLeft -gt $RenewWithinDays) { $Result.Action = "Current" $Result.Message = "Certificate valid for $DaysLeft more days" Write-Verbose "$($Device): $($Result.Message)" return $Result } $Action = "Renewed" #New alias so the web server keeps its certificate until the new one is bound $NewAlias = "$Alias-$(Get-Date -Format 'yyyyMMddHHmm')" } else { $Action = "Issued" $NewAlias = $Alias } ############################### ## Step 2: Subject and SAN ## ############################### Write-Verbose "$($Device): Resolving subject from template" Try { $Names = Get-AxisCertificateSubject -Device $Device } Catch { Throw "$($Device): Unable to build certificate subject: $($_.Exception.Message)" } $Result.Subject = $Names.Subject ############################### ## Step 3: Key and CSR ## ############################### Write-Verbose "$($Device): Creating key and CSR ($NewAlias)" $Param = @{ Device = $Device Alias = $NewAlias Subject = $Names.Subject SubjectAltNames = $Names.SubjectAltNames KeyType = $Config.Certificate.KeyType Keystore = $Config.Certificate.Keystore Force = $true } if($Authority.KeyTypes) { #The CA template may only accept some algorithms (e.g. RSA-only AD CS templates) $Param.Add('AllowedKeyTypes',$Authority.KeyTypes) } Try { $Request = New-AxisCertificateRequest @Param } Catch { Throw "$($Device): Unable to create certificate request: $($_.Exception.Message)" } ############################### ## Step 4: Submit to CA ## ############################### Write-Verbose "$($Device): Submitting CSR to $($Authority.Name) ($($Authority.Type))" Try { $Issued = Submit-AxisCertificateRequest -Authority $Authority -Csr $Request.Csr -Device $Device -TimeoutSec $Authority.TimeoutSec } Catch { #Leave the camera clean: the placeholder key is useless without a certificate (never the active alias) if(!$Existing -or $NewAlias -ne $Existing.Alias) { Try { Remove-AxisCertificate -Device $Device -Alias $NewAlias -Force } Catch {} Try { Remove-AxisCertificate -Device $Device -Alias "$NewAlias-csr" -Force } Catch {} } Throw "$($Device): Certificate authority rejected the request: $($_.Exception.Message)" } if($Issued.Status -ne "Issued") { Throw "$($Device): Certificate request $($Issued.RequestId) is $($Issued.Status) on $($Authority.Name)" } ############################### ## Step 5: Install and bind ## ############################### Write-Verbose "$($Device): Installing certificate" $Param = @{ Device = $Device Alias = $NewAlias Certificate = $Issued.Certificate CACertificates = $Issued.Chain AssignToHttps = $true HttpsOnly = $HttpsOnly } Try { $Installed = Install-AxisCertificate @Param } Catch { Throw "$($Device): Unable to install certificate: $($_.Exception.Message)" } $Result.Action = $Action $Result.Alias = $NewAlias $Result.Serial = $Issued.Serial $Result.NotAfter = $Installed.NotAfter $Result.Thumbprint = $Installed.Thumbprint $Result.HttpsVerified = $Installed.HttpsVerified $Result.Message = "$Action $($Request.KeyType) certificate" if(!$Installed.HttpsVerified) { $Result.Message += "; HTTPS binding could not be verified, old certificate kept" Write-Warning "$($Device): $($Result.Message)" return $Result } ############################### ## Step 6: Retire the old ## ############################### if($Existing) { #VERIFIED on Q1726 / AXIS OS 12.11 (2026-11-03): binding a new web server certificate restarts the web server, #so API calls are refused for a few seconds. Wait for the device to answer again before cleaning up. $Ready = $false ForEach($Attempt in 1..10) { Try { $Current = @(Get-AxisCertificate -Device $Device) $Ready = $true break } Catch { Write-Verbose "$($Device): web server not ready yet ($Attempt/10): $($_.Exception.Message)" Start-Sleep -Seconds 3 } } if(!$Ready) { Write-Warning "$($Device): web server did not come back within 30 seconds; old certificate $($Existing.Alias) left in place" $Result.Message += "; old certificate left in place (device busy)" return $Result } #Every other managed entry is superseded now: CA-issued predecessors (revoked below) and self-signed #placeholders left by interrupted earlier runs (just removed) $NewIssuer = (ConvertFrom-PemString -Pem $Issued.Certificate -AsCertificate | Select-Object -First 1).Issuer $Superseded = $Current | Where-Object { ($_.Alias -eq $Alias -or $_.Alias -like "$Alias-*") -and $_.Alias -ne $NewAlias -and $_.Alias -ne "$NewAlias-csr" } ForEach($Old in $Superseded) { Write-Verbose "$($Device): Removing superseded certificate $($Old.Alias)" Try { Remove-AxisCertificate -Device $Device -Alias $Old.Alias -Force } Catch { Write-Warning "$($Device): Could not remove old certificate $($Old.Alias): $($_.Exception.Message)" } if(!$NoRevoke -and $Old.Issuer -eq $NewIssuer -and $Old.Issuer -ne $Old.Subject -and $Old.Serial) { Try { $null = Revoke-AxisIssuedCertificate -Authority $Authority -Serial $Old.Serial -Reason Superseded $Result.RevokedSerial = (@($Result.RevokedSerial, $Old.Serial) | Where-Object { $_ }) -join "," $Result.Message += "; revoked $($Old.Serial) (Superseded)" } Catch { $Result.Message += "; revocation of $($Old.Serial) failed: $($_.Exception.Message)" Write-Warning "$($Device): $($Result.Message)" } } } } Write-Verbose "$($Device): $($Result.Message)" return $Result } |