Nutanix

Replace Nutanix SSL certificate with Let’s Encrypt certificate

1 min read 3 comments

NOTE: Script updated to work on the v4 api’s. Now only add the PC address and the registered clusters will get the certificate as well.

Here’s a powershell script to generate a Let’s Encrypt certificate and upload that certificate to your Nutanix cluster and/or Prism Central.

I want my clusters to be available via:

  • cluster-a.tielenconsultancy.nl
  • cluster-b.tielenconsultancy.nl
  • cluster-c.tielenconsultancy.nl
  • tielanix.tielenconsultancy.nl (This is Prism Central)

The tielenconsultancy.nl domain is avaiable on the internet and internal. The clusters aren’t available via the internet offcourse 😉

So this will only work if you have a public domain where you can add a cname record pointing to a code Let’s Encrypt will provide. On the internal DNS server you can make the a records pointing to clusters virtual ip’s.

Read the comments in the script, they will explain everything.

NOTE : I’m using a wildcard certificate. This is not recommended anymore ;).

<#
    This script will generate a Let's Encrypt certificate and will bind it to your Nutanix clusters and, if needed, to Prism Central.
    If the script is restarted a renewal will be done.

    THIS WILL ONLY WORK ON POWERSHELL 7.
    The certificate is uploaded with the v4 API via Prism Central, to Prism Central itself and to all the clusters registered to it.
    Clustermgmt v4.3 (Certificate Manager) is needed on Prism Central.

    Please change $DnsName and $PrismCentral to match your needs.

    Version    : 2.0
    Date       : 7 October 2026
    Created by : Jeroen Tielen - Tielen Consultancy B.V.
    Email      : jeroen@tielenconsultancy.nl

    History :
         1.0   : 5 May 2023 - Initial setup script.
         1.1   : 22 April 2024 - Typo's fixed. Renewal info added.
         2.0   : 6 October 2026 - Certificate upload changed from the v1 API to the v4 API. Contact removed, this broke the renewal.
#>

# Tune this variables to your need. The certificate will be assigned to Prism Central and to all the clusters registered to it.
$DnsName      = "*.yourdomain.tld"
$PrismCentral = "10.0.0.32"

# Stop script if PowerShell 7 isn't used.
If ($PSVersionTable.PSVersion.Major -lt 7) {
    Write-Host "PowerShell version 7 or higher is required."
    Exit
}

# Install posh-acme if not installed yet.
If (Get-Module -ListAvailable -Name Posh-ACME) {
    Write-Host "Module exists, skipping module installation."
} Else {
    Write-Host "Module does not exist, installing."
    Install-Module -name posh-acme -AcceptLicense
}

# This command will generate a new wildcard certificate. If this is the first time you run it, it will show you a cname record which is needed on your public DNS server.
# The check, after created the cname, can take up to two (2) minutes. Please be patient.
# Do not add -contact here. Let's Encrypt does not store contacts anymore and with it Posh-ACME creates a new account, and so a new cname record, on every run.
New-PACertificate $DnsName -accepttos -CertKeyLength 2048 -dnsplugin AcmeDns -PluginArgs @{ACMEServer='auth.acme-dns.io'} -Install

# Let's list the required certificate and fill the variable with it.
Get-PACertificate | Format-List
$Certificatefiles = Get-PACertificate

# Ask for your Nutanix credentials and construct the authorization header.
$Credentials = Get-Credential -Message "Enter your Prism Central credentials"
$UserName    = $Credentials.UserName
$Password    = $Credentials.GetNetworkCredential().Password
$AuthHeader  = @{
    Accept        = "application/json"
    Authorization = "Basic " + [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($UserName + ":" + $Password))
}

# Generate the payload to upload via the v4 API to the clusters. The v4 API wants json with the files base64 encoded.
$Body = @{
    privateKeyAlgorithm = "RSA_2048"
    privateKey          = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Certificatefiles.KeyFile))
    publicCertificate   = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Certificatefiles.CertFile))
    caChain             = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Certificatefiles.ChainFile))
} | ConvertTo-Json

# Prism Central wants the complete chain including the root certificate. Let's Encrypt doesn't deliver the root (ISRG Root X1), so download it and put it behind the chain.
$RootFile = Join-Path ([System.IO.Path]::GetTempPath()) "isrgrootx1.pem"
Invoke-WebRequest -Uri "https://letsencrypt.org/certs/isrgrootx1.pem" -OutFile $RootFile
$ChainPC  = (Get-Content -Raw -Path $Certificatefiles.ChainFile).Trim() + "`n" + (Get-Content -Raw -Path $RootFile).Trim() + "`n"

# Generate the payload to upload via the v4 API to Prism Central. Prism Central uses the Certificate Manager and that one has other names for the same files.
$BodyPC = @{
    privateKey       = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Certificatefiles.KeyFile))
    certificate      = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Certificatefiles.CertFile))
    certificateChain = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($ChainPC))
} | ConvertTo-Json

# Get all the clusters from Prism Central. Prism Central is in this list too, put it at the end because his gui restarts after the certificate is replaced.
$ApiUrl   = "https://" + $PrismCentral + ":9440/api"
$Clusters = (Invoke-RestMethod -Method Get -Uri "$ApiUrl/clustermgmt/v4.1/config/clusters?`$limit=100" -Headers $AuthHeader -SkipCertificateCheck).data
$Clusters = $Clusters | Sort-Object { $_.config.clusterFunction -contains "PRISM_CENTRAL" }
$Wait     = $False

# Assign the certificate to the clusters and Prism Central. Everything goes via Prism Central, Prism Element doesn't have these v4 API's.
ForEach ($Cluster in $Clusters) {
    Write-Host " Replacing certificate on " -NoNewline
    Write-Host $Cluster.name -ForegroundColor Yellow
    $Task = $Null

    If ($Cluster.config.clusterFunction -contains "PRISM_CENTRAL") {
        # Prism Central keeps his certificate in the Certificate Manager (clustermgmt v4.3). The ssl-certificate call below fails there with "the ETag provided doesn't match".
        # Add ?`$dryrun=true behind the url if you only want Prism Central to validate the certificate.
        $CertUrl = "$ApiUrl/clustermgmt/v4.3/config/clusters/" + $Cluster.extId + "/certificates"
        $Headers = $AuthHeader + @{ "NTNX-Request-Id" = (New-Guid).Guid }
        $Task    = (Invoke-RestMethod -Method Post -Uri $CertUrl -Body $BodyPC -ContentType "application/json" -Headers $Headers -SkipCertificateCheck).data
    } Else {
        # This call is allowed only once per minute, so wait a minute when there was a cluster before this one.
        If ($Wait) {
            Write-Host " Waiting a minute, the API allows only one certificate per minute."
            Start-Sleep -Seconds 60
        }
        $Wait = $True

        # Get the current certificate, the ETag of it is needed to be allowed to replace it.
        $CertUrl = "$ApiUrl/clustermgmt/v4.1/config/clusters/" + $Cluster.extId + "/ssl-certificate"
        $Current = Invoke-RestMethod -Method Get -Uri $CertUrl -Headers $AuthHeader -SkipCertificateCheck -ResponseHeadersVariable ResponseHeaders
        $Headers = $AuthHeader + @{
            "If-Match"        = $ResponseHeaders.ETag | Select -First 1
            "NTNX-Request-Id" = (New-Guid).Guid
        }

        # Upload the certificate. The -SkipHeaderValidation is needed because PowerShell doesn't like the format of the Nutanix ETag.
        $Task = (Invoke-RestMethod -Method Put -Uri $CertUrl -Body $Body -ContentType "application/json" -Headers $Headers -SkipCertificateCheck -SkipHeaderValidation).data
    }

    # The upload starts a task. No task means the upload failed.
    If ($Null -eq $Task) {
        Write-Host " Upload failed, skipping " -ForegroundColor Red -NoNewline
        Write-Host $Cluster.name -ForegroundColor Red
        Continue
    }

    # Wait for the task to finish. You can get an error here when the gui restarts with the new certificate, just be patient ;)
    Do {
        Start-Sleep -Seconds 5
        $TaskInfo = (Invoke-RestMethod -Method Get -Uri ("$ApiUrl/prism/v4.0/config/tasks/" + $Task.extId) -Headers $AuthHeader -SkipCertificateCheck).data
        Write-Host " Task status: " -NoNewline
        Write-Host $TaskInfo.status -ForegroundColor Yellow
    } While ($TaskInfo.status -eq "QUEUED" -or $TaskInfo.status -eq "RUNNING")

    # Show why the task failed, for example when the certificate is not accepted.
    If ($TaskInfo.status -eq "FAILED") { Write-Host $TaskInfo.errorMessages.message -ForegroundColor Red }
}

Nutanix .NEXT 2027, Las Vegas, March 29 to April 1, 2027: registration is live

3 comments

  1. Hi,

    you could replace this:
    # Stop script if PowerShell 7 isn’t used.
    If ($PSVersionTable.PSVersion.Major -lt 7) {
    Write-Host “PowerShell version 7 or higher is required.”
    Exit
    }

    # Install posh-acme if not installed yet.
    If (Get-Module -ListAvailable -Name Posh-ACME) {
    Write-Host “Module exists, skipping module installation.”
    } Else {
    Write-Host “Module does not exist, installing.”
    Install-Module -name posh-acme -AcceptLicense
    }

    with:
    #Requires -Version 7
    #Requires -Modules Posh-ACME

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.