Ryan Bolger

Ryan Bolger

Adventures In Tech

Automating Free Certs for Infoblox Grid Manager

Posh-ACME and Posh-IBWAPI, better together

Ryan Bolger

HTTPS web UIs and APIs using self-signed certificates have always annoyed me. It’s one of the reasons I started learning about PKI long before Let’s Encrypt and the ACME protocol made free publicly trusted certs available for everyone. But it may still be a while before large products and platforms like Infoblox start natively supporting ACME to make our self-signed cert woes a thing of the past. Until then, here’s how you can use Posh-ACME and Posh-IBWAPI to get a free cert for your Grid Manager and automate the renewals.

NIOS Version Caveats

Infoblox’s NIOS hasn’t always had the best support for modern certificate standards. It wasn’t until NIOS 8.3 that the Subject Alternative Name field was supported. HSTS support was added sometime in 8.4. And even in the latest versions of NIOS 8.6, ECC based certs and associated cipher suites still aren’t supported.

So when it comes to certificate support, the newer your NIOS version the better. But I’d advise being on at least NIOS 8.3 before messing too much with this.

Public Cert Caveats

Most folks running an Infoblox grid aren’t exposing their Grid Manager interface to the Internet. But in order to get a certificate from a public CA like Let’s Encrypt, the FQDN in the cert must be part of a domain that was obtained from an ICANN recognized domain registrar. If your grid manager FQDN ends in a TLD like .local or .int, you’re out of luck. That also means no short names in URLs like https://mygrid/ui.

The other thing to consider is that public CAs must send all of their certificates to publicly available logs due to a standard called Certificate Transparency. So if your organization insists on trying to keep the names of your internal systems secret, you’ll have to stick to an internal CA. Though wildcard certs (*.example.com) can be an acceptable compromise in some cases.

It should be noted that some private CAs such as smallstep have native ACME support. So you may still be able to use Posh-ACME depending on your environment.

Infoblox Setup

One of the ways ACME CAs can validate that you own/control the domain you’re requesting a cert for is via DNS TXT records. For the purposes of this article, we’re going to assume your grid manager is called mygrid.poshac.me and the public facing DNS zone that FQDN is hosted on Infoblox in a DNS view called external.

NOTE: If your public DNS zone is hosted somewhere else like a dedicated DNS provider or your domain registrar, you’ll need to check the Posh-ACME DNS plugins list to see if your provider is supported..

Under normal circumstances, you will need to add and remove a record for each name in a cert each time a new cert is obtained which will be approximately every 60-90 days with Let’s Encrypt. Since the goal here is automation, it is highly advisable to use a dedicated account with the least privileges necessary rather than your own admin credentials. That account will also need write access to the grid manager in order to manage its certificate. So let’s create an admin group with the appropriate permissions.

If you’re new to Posh-IBWAPI, head over to the quick start to learn how to setup a profile for your admin credentials before moving on. Don’t forget the -SkipCertificateCheck flag because presumably your grid manager doesn’t have a valid certificate yet. Then run the following to create a new group that grants TXT editing privileges to the necessary zone and write access to the grid manager.

# create the empty group
$grpTemplate = @{
    name = 'Grid Manager Cert Updater'
    comment = 'Allows TXT editing on poshac.me in the external DNS view'
    access_method = @('API')
}
New-IBObject -ObjectType admingroup -IBObject $grpTemplate

# grab an object reference to the zone and grid manager
# (filters are case-sensitive)
$zone = Get-IBObject -ObjectType zone_auth -Filters 'fqdn=poshac.me','view=external'
$gm = Get-IBObject -ObjectType member -Filters 'host_name=mygrid.poshac.me'

# add the permissions to the group
$perms = @(
    @{ permission='WRITE'; resource_type='TXT'; object=$zone.'_ref'; group=$grpTemplate.name }
    @{ permission='WRITE'; object=$gm.'_ref'; group=$grpTemplate.name }
)
$perms | New-IBObject -ObjectType permission

The account can be local or remote. But I prefer a local account since this will be responsible for maintaining the grid itself. We don’t want temporary problems with external account authentication to have cascading effects like failing to renew the grid manager certificate. Run the following to create an account which is a member of the group we previously created.

$usrTemplate = @{
    name = 'cert-updater'
    password = (New-Guid).ToString() # use a password compliant with your org policies
    admin_groups = @($grpTemplate.name)
    comment = 'Used to update the grid manager certificate'
    auth_type = 'LOCAL'
}
New-IBObject -ObjectType adminuser -IBObject $usrTemplate
Write-Host "cert-updater user created with password $($usrTemplate.password)"

Here’s what the resulting group permissions look like in the UI in my lab environment.

GUI view of Grid Manager Cert Updater permissions

Test TXT Record Publishing

If you’re not familiar with Posh-ACME or haven’t previously used it for DNS challenges against Infoblox, it is wise to verify the TXT record publishing is working as intended before trying to get a certificate. Configure Posh-ACME to use the Let’s Encrypt Staging server and create an account if one doesn’t already exist.

Set-PAServer -DirectoryUrl LE_STAGE
New-PAAccount -AcceptTOS

Next build the hashtable we’ll need to use for Posh-ACME’s Infoblox plugin and test that it works to publish a TXT record in the appropriate zone.

# create the plugin args
$pArgs = @{
    IBServer = 'mygrid.poshac.me'
    IBCred = (Get-Credential -UserName 'cert-updater')
    IBView = 'external'
    IBIgnoreCert = $true
}

# publish the record using the plugin args and our ACME account
$publishParams = @{
    Domain = 'mygrid.poshac.me'
    Account = (Get-PAAccount)
    Token = 'fake-token'
    Plugin = 'Infoblox'
    PluginArgs = $pArgs
    Verbose = $true
}
Publish-Challenge @publishParams

Assuming there were no errors, you should be able to check the Infoblox GUI for the _acme-challenge.mygrid.poshac.me TXT record that should now exist. Alternatively, you can query it via Posh-IBWAPI like this:

Get-IBObject 'record:txt' -Filters 'view=external','name=_acme-challenge.mygrid.poshac.me' | Format-List

If everything looks good, unpublish the record:

Unpublish-Challenge @publishParams

Building the Renewal Script

Many folks don’t realize that with ACME there’s no difference between an initial certificate request and a renewal. It often feels different because the initial request tends to involve more effort setting things up and configuring credentials. But the ACME client usually saves that config data and reuses it during a “renewal” to request a new certificate from scratch. With this in mind, we’re going to construct our script to work properly whether it’s the first run or the 100th run. The only prerequisite we’re going to depend on is a Posh-IBWAPI profile using our cert-updater credentials. Run the following to set that up:

$configParams = @{
    ProfileName = 'certs'
    WAPIHost = 'mygrid.poshac.me'
    WAPIVersion = 'latest'
    Credential = (Get-Credential -UserName 'cert-updater')
    SkipCertificateCheck = $true
}
Set-IBConfig @configParams

NOTE: If you’re not currently running PowerShell as the user who will be executing the renewal script, you’ll need to remember to do this step for that user when setting up the scheduled task.

The high level tasks in our script include:

  • Decide whether to get a new certificate
  • Generate the certificate request (CSR)
  • Request the certificate using the CSR
  • Upload the certificate to Infoblox

Setup and Renewal Time Logic

Renewal scripts like this are usually scheduled to run once or twice a day and have logic to exit early if the previous certificate is too new to replace. The Submit-Renewal command in Posh-ACME has that renewal time frame logic built-in, but in this case we won’t be able to use it because we also need to generate a fresh CSR prior to calling the renewal and we don’t necessarily want to create a new CSR until we know we need it. First, let’s build the skeleton of the script including a -DryRun switch that will do everything against the Let’s Encrypt Staging server and skip the step that actually uploads the certificate to Infoblox.

#Requires -Modules Posh-ACME,Posh-IBWAPI

[CmdletBinding()]
param(
    [string]$ContactEmail,
    [switch]$DryRun
)

# make sure we have a Posh-IBWAPI config
$ibConfig = Get-IBConfig
if (-not $ibConfig) {
    throw "No Infoblox profile found for Posh-IBWAPI. Please use Set-IBConfig to configure one."
}

# make sure we're using the correct ACME server
if (-not $DryRun) {
    Set-PAServer LE_PROD
} else {
    Set-PAServer LE_STAGE
}

# find or create an ACME account
if (-not ($acmeAcct = Get-PAAccount)) {
    if ($acmeAcct = (Get-PAAccount -List | Select-Object -First 1)) {
        $acmeAcct | Set-PAAccount
    } elseif ($ContactEmail) {
        $acmeAcct = New-PAAccount -AcceptTOS -Contact $ContactEmail
    } else {
        $acmeAcct = New-PAAccount -AcceptTOS
    }
}

# check for an existing order and make sure it's time to renew
$order = Get-PAOrder -MainDomain $ibConfig.WAPIHost
if (-not $order -or -not $order.RenewAfter -or
    (Get-Date) -ge (Get-Date $order.RenewAfter))
{
    # TODO: Generate CSR

    # TODO: Request Cert

    if (-not $DryRun) {

        # TODO: Upload to Infoblox

    } else {
        Write-Verbose "DryRun skipping cert upload to Infoblox"
    }

} else {
    Write-Verbose "Certificate for $($ibConfig.WAPIHost) is not ready to renew."
}

The basic script flow is done, but we’ve left some TODO placeholders that we’ll replace with our implementations that we’ll build in the next few sections.

Generate the Certificate Request

Typically, ACME clients like Posh-ACME automatically generate the private key that will be associated with the certificate because the client usually runs on the same device where the certificate will be used. While we can do that with Infoblox and upload the private key along with the certificate, we can also use its native CSR generation process and let the grid manager deal with generating and securing the private key. Posh-ACME supports both workflows, but this is a good excuse to demonstrate a rarely used WAPI function.

At this point in our script, we’re assuming Posh-IBWAPI is already configured and we can call WAPI’s generatecsr function which is associated with the fileop object. Calling the function does not change anything about the currently live certificate. That only happens when you successfully upload a new cert based on the CSR downloaded from the function call.

# generate a temp file we can save the CSR to
$csrFile = New-TemporaryFile

# grab the name of our grid manager from the Posh-IBWAPI config
$gm = $ibConfig.WAPIHost

# call generatecsr
$reqParams = @{
    FunctionName = 'generatecsr'
    FunctionArgs = @{
        certificate_usage = 'ADMIN'    # aka Web UI
        member = $gm
        cn = $gm
        subject_alternative_names = @(
            @{ type='DNS'; value=$gm }
        )
    }
    OutFile = $csrFile
    OverrideTransferHost = $true
}
Receive-IBFile @reqParams

NOTE: Free ACME CAs like Let’s Encrypt will ignore the metadata in a certificate request (Organization, Country, etc.) except the domain name because they only offer Domain Validated certificates. So we don’t have to add any of those fields in our function call.

We’re using the WAPIHost property of our current Posh-IBWAPI profile as the grid manager’s FQDN and member name. In some environments, the grid manager’s member name may be different than the DNS FQDN folks use to connect to it. In that case, modify the member = $gm line to reflect the grid manager’s actual member name. You can also add additional names to the subject_alternative_names array if there are additional names needed in the cert.

Request the Certificate

With the CSR generated, we can move on to requesting the certificate from Let’s Encrypt. We’ll just need to copy some of the properties from the Posh-IBWAPI profile into New-PACertificate’s PluginArgs parameter. This gets a bit more complicated if you’re using a non-Infoblox DNS plugin since the plugin parameters won’t be so easily accessible. But it shouldn’t change the overall shape of the script too much.

try {
    # request the certificate and copy the necessary PluginArgs from
    # the Posh-IBWAPI config
    $certParams = @{
        CSRPath = $csrFile.FullName
        Plugin = 'Infoblox'
        PluginArgs = @{
            IBServer = $ibConfig.WAPIHost
            IBCred = $ibConfig.Credential
            IBView = 'external'
            IBIgnoreCert = $true
        }
        Verbose = $true
        Force = $true
        ErrorAction = 'Stop'
    }
    $cert = New-PACertificate @certParams
}
finally {
    $csrFile | Remove-Item
}

Upload the Certificate

In the previous section, we saved our certificate details into a $cert variable which we can now use to upload the cert file to Infoblox with the uploadcertificate WAPI function.

# upload the cert
$uploadParams = @{
    FunctionName = 'uploadcertificate'
    Path = $cert.CertFile
    FunctionArgs = @{
        certificate_usage = 'ADMIN'
        member = $gm
    }
    OverrideTransferHost = $true
}
Write-Verbose "Uploading cert to $gm"
Send-IBFile @uploadParams

NOTE: Remember that this section of code won’t run when you call the script using the -DryRun parameter. So remove it if you’ve been testing the certificate generation until now.

The uploadcertificate function has no output unless there was a problem and the grid manager will automatically restart the web GUI to start using the new cert. However, if you have an existing web browser open to the grid it may hang onto the previously cached self-signed cert for a while (particularly Google Chrome). If you find this is happening, try closing the tab and opening a new one, using Incognito mode, or using a different browser altogether. Eventually, you should be able to verify the grid manager is using the new certificate.

Putting it all together

Our basic script is now finished. There are a variety of things you could do to improve it like more error handling and better logging, but it should be a good starting point. When running it, don’t forget to include the -ContactEmail me@example.com parameter if you want Let’s Encrypt to send you email reminders if the cert gets close to expiring without having renewed it yet.

Here is the completed code all in one place.

#Requires -Modules Posh-ACME,Posh-IBWAPI

[CmdletBinding()]
param(
    [string]$ContactEmail,
    [switch]$DryRun
)

# make sure we have a Posh-IBWAPI config
$ibConfig = Get-IBConfig
if (-not $ibConfig) {
    throw "No Infoblox profile found for Posh-IBWAPI. Please use Set-IBConfig to configure one."
}

# make sure we're using the correct ACME server
if (-not $DryRun) {
    Set-PAServer LE_PROD
} else {
    Set-PAServer LE_STAGE
}

# find or create an ACME account
if (-not ($acmeAcct = Get-PAAccount)) {
    if ($acmeAcct = (Get-PAAccount -List | Select-Object -First 1)) {
        $acmeAcct | Set-PAAccount
    } elseif ($ContactEmail) {
        $acmeAcct = New-PAAccount -AcceptTOS -Contact $ContactEmail
    } else {
        $acmeAcct = New-PAAccount -AcceptTOS
    }
}

# check for an existing order and make sure it's time to renew
$order = Get-PAOrder -MainDomain $ibConfig.WAPIHost
if (-not $order -or -not $order.RenewAfter -or
    (Get-Date) -ge (Get-Date $order.RenewAfter))
{
    # generate a temp file we can save the CSR to
    $csrFile = New-TemporaryFile

    # grab the name of our grid manager from the Posh-IBWAPI config
    $gm = $ibConfig.WAPIHost

    # call generatecsr
    $reqParams = @{
        FunctionName = 'generatecsr'
        FunctionArgs = @{
            certificate_usage = 'ADMIN'    # aka Web UI
            member = $gm
            cn = $gm
            subject_alternative_names = @(
                @{ type='DNS'; value=$gm }
            )
        }
        OutFile = $csrFile.FullName
        OverrideTransferHost = $true
    }
    Receive-IBFile @reqParams

    try {
        # request the certificate and copy the necessary PluginArgs from
        # the Posh-IBWAPI config
        $certParams = @{
            CSRPath = $csrFile.FullName
            Plugin = 'Infoblox'
            PluginArgs = @{
                IBServer = $ibConfig.WAPIHost
                IBCred = $ibConfig.Credential
                IBView = 'external'
                IBIgnoreCert = $true
            }
            Verbose = $true
            Force = $true
            ErrorAction = 'Stop'
        }
        $cert = New-PACertificate @certParams
    }
    finally {
        $csrFile | Remove-Item
    }

    if (-not $DryRun) {

        # upload the cert
        $uploadParams = @{
            FunctionName = 'uploadcertificate'
            Path = $cert.CertFile
            FunctionArgs = @{
                certificate_usage = 'ADMIN'
                member = $gm
            }
            OverrideTransferHost = $true
        }
        Write-Verbose "Uploading cert to $gm"
        Send-IBFile @uploadParams

    } else {
        Write-Verbose "DryRun skipping cert upload to Infoblox"
    }

} else {
    Write-Verbose "Certificate for $($ibConfig.WAPIHost) is not ready to renew."
}

Recent Posts

Categories