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:
DEC750v2

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:
DEC750v2

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

Hi all,

I have a similar/related problem trying to achieve the same thing.
I use a python script and the "requests" module to update the certificate, and the critical part looks like this:
try:
    basic_auth = requests.auth.HTTPBasicAuth(tokenname, token)
   
    cert_uuid = "ed543dbb-6b81-4c92-b831-cd678214a853"
    cert_data = {
        "cert": {
            "action": "import",
            "descr": "MyCertDescription",
            "cert_type": "usr_cert",
            "private_key_location": "firewall",
            "crt_payload": cert, # string content of certificate file
            "prv_payload": key, # string content of privkey file
            "csr_payload": ""
        }
    }

    req_url1 = 'https://' + api_url + '/api/trust/cert/set/' + cert_uuid
    req_url2 = 'https://' + api_url + '/api/trust/cert/add'
    req = requests.post(req_url1, auth=basic_auth, data=cert_data)
    resp = req.json()

This sadly returns the following error:

{'errorMessage': 'missing CA key\n error:0480006C:PEM routines::no start line error:0480006C:PEM routines::no start line', 'errorTitle': 'Certificate error'}

And when I just try to upload the certificate as a new item instead of replacing the old one (so using req_url2 instead of req_url1), it returns the following error:

{'result': 'failed', 'validations': {'cert.descr': 'A value is required.'}}

Can anybody help me on this one? It feels like I'm quite close, but somewhere there must be a mistake...

I've already tried/checked the following:
* The API access works (Other API calls work, necessary permissions are granted)
* The certificate itself is correct (wildcard certificate, also used in several other places without problems)
* The contents of cert/privkey are read correctly from the files and the variables "cert" and "key" contain the correct data ("-----BEGIN CERTIFICATE-----" etc etc)
* The certificate UUID comes from the Web UI: Opening the "info" popup for the old certificate and watched in browser's dev tools, which API URL was called --> it contained the UUID
* I even once "replayed" an entire API call from the Web UI that worked there, by copying the URL and POST parameters object from the dev tools to the python code. Still the same errors.

I'd be glad to share the script once it is working.
Any help is much appreciated!

The key part in my script was this:
$allPem  = (Get-Content -Raw $FullchainPath) -replace "`r`n","`n"

I had to replace \r\n with just \n before mine would import.

Worth a try......
Hardware:
DEC750v2

Thanks for the quick reply!

"Unfortunately" the error (and I) was way more dumb: The HTTP request did not have the correct Content-Type header because I called the request library with "data=" instead of "json=". Once I changed this, the API calls worked fine.

Sorry for the unnecessary question then, but thanks so much anyway for taking the time to reply!