change the Web UI certificate via the API.

Started by jjrushford, November 15, 2025, 12:19:24 AM

Previous topic - Next topic
Greetings,

I run acme scripts outside of OpnSense to get and update a wildcard certificate for my domain.  I use this certificate for various services and I automate their installation via the acme post installation hooks.

I have a tool that lets me import the certificate to OpnSense using the API but, I cannot find the endpoint and required parameters to update the Administration settings to use the newly imported certificate in the Web UI.  Is there an API endpoint to do this?  I'm running OPNsense 25.1.12

thanks
John

Hi

I am looking for that too. Unfortunately I still have no answer.
According to the API reference(here: OPNsense API Reference), there should be the action "set", which my intuition says that it's "setting a certificate to a certain status/value". AI suggested that, in order to update an existing certificate, I should send POST request identical as the "add" one, but simply send it to https://<my_opnsense>/api/trust/cert/set/<uuid_of_the_existing_cert> . I've tried that, and many other combination like uuid field in the body or in the URL in a PHP fashion(?uuid=<uuid_of_the_existing_cert>), but nothing worked.

I am out of clues after exhausting all I could get from google and AI

What I do is just replace the existing one via the api and restart the webgui.  I think it matches on description.

I have scripts to do exactly this, but I can't share them until I'm back at work in January.
Hardware:
DEC750

Hi, thanks. I'll remind you about it :)


may I ask, are your scripts using API only, or you're doing it the PHP way?

Thx.

McCasian

Powershell scripts. Let's see how this looks

param($result)

# ----- EDIT THESE -----
$FullchainPath = '/usr/share/certify/alanplum.crt'
$PrivKeyPath   = '/usr/share/certify/alanplum.key'
$DescrCommon   = 'CertifyTheWeb Wildcard'

$Targets = @(
  @{ hostName='hide.me.net'; key='hidden'; secret='hidden' }
)
# --------------------------------

# Ensure PowerShell 7+ for -SkipCertificateCheck
if (-not ($PSVersionTable.PSVersion.Major -ge 7)) {
  throw "This script requires PowerShell 7+ for -SkipCertificateCheck. Current: $($PSVersionTable.PSVersion)"
}

# Read PEMs once; use LEAF cert
if (!(Test-Path $FullchainPath)) { throw "Fullchain not found: $FullchainPath" }
if (!(Test-Path $PrivKeyPath))   { throw "Private key not found: $PrivKeyPath" }

$allPem  = (Get-Content -Raw $FullchainPath) -replace "`r`n","`n"
$LeafPem = [regex]::Match(
  $allPem,
  '-----BEGIN CERTIFICATE-----(?:.|\n)+?-----END CERTIFICATE-----'
).Value

$KeyPem  = (Get-Content -Raw $PrivKeyPath) -replace "`r`n","`n"

function Invoke-OpnAddOrUpdate {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)][string]$HostName,
    [Parameter(Mandatory)][string]$Key,
    [Parameter(Mandatory)][string]$Secret,
    [Parameter(Mandatory)][string]$Descr,
    [Parameter(Mandatory)][string]$Leaf,
    [Parameter(Mandatory)][string]$Prv
  )

  $base   = "https://$HostName"
  $pair   = "${Key}:${Secret}"
  $basic  = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
  $headers = @{
    Authorization = $basic
    Accept        = 'application/json'
  }

  # 1) Search existing by description
  $uuid = $null
  try {
    $search = Invoke-RestMethod -Method Get -Uri "$base/api/trust/cert/search" `
              -Headers $headers -SkipCertificateCheck
    if ($search -and $search.rows) {
      $row = $search.rows | Where-Object { $_.descr -eq $Descr } | Select-Object -First 1
      if ($row) { $uuid = $row.uuid }
    }
  } catch {
    $status = $_.Exception.Response.StatusCode.value__ 2>$null
    $msg    = if ($_.ErrorDetails.Message) { $_.ErrorDetails.Message } else { $_.Exception.Message }
    Write-Warning ("[FAIL] {0} search (HTTP {1}) {2}" -f $HostName,$status,$msg)
    return
  }

  # 2) Build payload (PEMs via *_payload)
  $payload = @{
    cert = @{
      action               = 'import'
      descr                = $Descr
      cert_type            = 'usr_cert'
      private_key_location = 'firewall'
      crt_payload          = $Leaf
      prv_payload          = $Prv
      csr_payload          = ''
    }
  } | ConvertTo-Json -Depth 8

  # 3) Update if found, else add
  if ($uuid) { $url = "$base/api/trust/cert/set/$uuid"; $action='UPDATED' }
  else       { $url = "$base/api/trust/cert/add"      ; $action='ADDED'  }

  try {
    $null = Invoke-RestMethod -Method Post -Uri $url -Headers $headers `
              -ContentType 'application/json' -Body $payload -SkipCertificateCheck
    Write-Host ("[OK]   {0} {1}" -f $HostName,$action)
  } catch {
    $status = $_.Exception.Response.StatusCode.value__ 2>$null
    $msg    = if ($_.ErrorDetails.Message) { $_.ErrorDetails.Message } else { $_.Exception.Message }
    Write-Warning ("[FAIL] {0} (HTTP {1}) {2}" -f $HostName,$status,$msg)
  }
}

foreach ($t in $Targets) {
  try {
    $descr = if ($t.ContainsKey('descr') -and $t.descr) { $t.descr } else { $DescrCommon }
    Invoke-OpnAddOrUpdate -HostName $t.hostName -Key $t.key -Secret $t.secret `
                          -Descr $descr -Leaf $LeafPem -Prv $KeyPem
  } catch {
    Write-Warning ("[FAIL] {0} unexpected error: {1}" -f $t.hostName, $_.Exception.Message)
  }
}

That's was hard to get from my iPad at home :)

Hardware:
DEC750

December 26, 2025, 11:57:40 PM #5 Last Edit: December 27, 2025, 12:01:49 AM by mccasian
Hi ProximusAl

You made my day, thank you very much. I can now update the firewall certificate in place. It still does not solve changing the Web UI certificate via the API, but once the correct certificate is selected in the UI, being able to renew it in place through the API is sufficient for me.

For anyone looking for the cURL way of updating the certificate in-place, here it is:
###
POST https://opnsense.example.com/api/trust/cert/set/<cert_uuid> HTTP/1.1
Authorization: Basic {{key}}:{{secret}}
Content-Type: application/json

{"cert":
    {
        "action":"import",
        "descr":"dummy_description",
        "cert_type":"usr_cert",
        "private_key_location":"firewall",
        "crt_payload":"-----BEGIN CERTIFICATE-----\n[...]\n-----END CERTIFICATE-----",
        "prv_payload":"-----BEGIN PRIVATE KEY-----\n[...]\n-----END PRIVATE KEY-----",
        "csr_payload":""
    }   
}


Best regards
Casian