VCF 9.1 Fleet Management API (1): Certificate Management with Custom CA

Welcome to the first entry in our VCF 9.1 Fleet Management Blog Series! In this series, we will explore how to leverage the power of the Fleet Management APIs in VMware Cloud Foundation (VCF) 9.1 to automate day-to-day infrastructure operations, minimize human error, and achieve true cloud-scale efficiency.

Today, we kick things off by tackling one of the most critical—and traditionally tedious—operational tasks in the private cloud: Certificate Lifecycle Management.

Out-of-the-Box CA Integrations

VCF 9.1 introduces powerful native integrations with enterprise Certificate Authorities like Microsoft CA. When natively configured, VCF can manage the end-to-end certificate lifecycle out of the box, including highly anticipated features like non-disruptive automated renewal.

However, not all organizations utilize these natively supported CAs. To achieve complete certificate lifecycle automation in these environments, the VCF Fleet Management API can be leveraged.

Let’s dive into how to execute this by walking through a multi-step workflow to replace an expiring vCenter Server Machine SSL certificate using the API.

Step-by-Step Guide: Replacing a vCenter Certificate via API

Setup:

VCF 9.1

VCF Operations FQDN: flt-ops01a.rainpole.io

Step 1: Fetch the resourceKey for vCenter certificate

Before generating a Certificate Signing Request (CSR) for my vCenter (sfo-m01-vc01.sfo.rainpole.io), we must identify the VCF Operation resource identifier (resourceKey or vmid) of the vCenter Server’s TLS certificate.

API Request:

curl –request POST \
–url https://flt-ops01a.rainpole.io/suite-api/api/fleet-management/certificate-management/certificates/query \
–header ‘accept: application/json’ \
–header ‘authorization: Bearer {{apiAccessToken}}’ \
–header ‘content-type: application/json’ \
–data ‘{
“status”: “NORMAL”,
“appliance”: “VCENTER”,
“applianceFqdn”: “sfo-m01-vc01.sfo.rainpole.io”,
“category”: “TLS_CERT”
}’

You possibly noticed that I am using the VCF SSO Access token for authentication. If you are not familiar with the process, please check VCF 9.1 API Access (3): Using API Access Token for NSX and Operations API.

Response:

{
"pageInfo": {
"totalCount": 1,
"page": 0,
"pageSize": 1000
},
...
"vcfCertificateModels": [
{
"certificateResourceKey": "4d5949ec-e922-32f5-8449-818455bf7522",
"issuedTo": "C=US, CN=sfo-m01-vc01.sfo.rainpole.io",
"appliance": "VCENTER",
"displayApplianceType": "vCenter",
"category": "TLS_CERT",
"type": "VMCA",
"issuedBy": "OU=VMware Engineering,O=sfo-m01-vc01.sfo.rainpole.io,ST=California,C=US,DC=local,DC=vsphere,CN=CA",
"status": "NORMAL",
"displayStatus": "Active",
"daysToExpire": 729,
"expiryDate": 1841709522000,
"subjectAlternativeNames": {
"dns": [
"sfo-m01-vc01.sfo.rainpole.io"
],
"ip": []
},
"applianceFqdn": "sfo-m01-vc01.sfo.rainpole.io",
"redirectAvailable": true,
"domainId": "50337e83-cd5e-40a4-9328-64aec3937805",
"vcfEndpoint": "sfo-vcf01.sfo.rainpole.io",
"vcfComponent": "SDDC",
"issuedToCommonName": "sfo-m01-vc01.sfo.rainpole.io",
"autoRenewInfo": {
"autoRenewStatus": "DISABLED",
"autoRenewFailureReason": ""
},
"autoRenewState": "DISABLED",
"displayAutoRenewState": "Deactivated"
}
]
}

Let us extract the certificateResourceKey, applianceFqdn and subjectAlternativeNames from the response as input for the Certificate Signing Request (CSR) generation API.

Step 2: Generate CSR

Armed with the certificateResourceKey, applianceFqdn and subjectAlternativeNames, we are ready to generate a CSR for the vCenter certificate.

API Request

curl --request POST \
--url https://flt-ops01a.rainpole.io/suite-api/api/fleet-management/certificate-management/csrs \
--header 'accept: application/json' \
--header 'authorization: Bearer {{apiAccessToken}}' \
--header 'content-type: application/json' \
--data '{
"certificateId": "{{certificateResourceKey}}",
"generateCsrSpec": {
"commonName": "{{applianceFqdn}}",
"country": "US",
"email": "",
"keySize": "KEY_2048",
"keyAlgorithm": "RSA",
"locality": "Palo Alto",
"organization": "VMware",
"orgUnit": "VMware Engineering",
"state": "California",
"subjectAltNames": {{subjectAlternativeNames}}
}
}'

Response:

{
"requestId": "3e897fe1-f979-4279-893c-7c3a054e05f1",
"requestName": "ucmgeneratecsrworkflow",
"requestReason": "Generate Certificate Signing Request (CSR) API",
"requestType": "Generate certificate signing request",
"state": "INPROGRESS",
"errorCause": [],
"duration": 141,
"category": "VCF_CERTIFICATE_MANAGEMENT"
}

Extract the requestId as input for the next API call.

Step 3: Track CSR generation status

API Request

curl --request GET \
--url https://flt-ops01a.rainpole.io/suite-api/api/workflows/requests/{{requestId}} \
--header 'accept: application/json' \
--header 'authorization: Bearer {{apiAccessToken}}' \
--header 'content-type: application/json'

Response:

{
"requestId": "3e897fe1-f979-4279-893c-7c3a054e05f1",
"requestName": "ucmgeneratecsrworkflow",
"requestReason": "Generate Certificate Signing Request (CSR) API",
"requestType": "Generate certificate signing request",
"state": "COMPLETED",
"errorCause": [],
"duration": 5857,
"category": "VCF_CERTIFICATE_MANAGEMENT"
}

From the above response, I can see the CSR generation has completed successfully.

Step 4: Get generated CSR

API:

curl --request GET \
--url 'https://flt-ops01a.rainpole.io/suite-api/api/fleet-management/certificate-management/csrs?commonName=sfo-m01-vc01.sfo.rainpole.io' \
--header 'accept: application/json' \
--header 'authorization: Bearer {{apiAccessToken}}' \
--header 'content-type: application/json'

I use commonName as a filter for this Get API.

Response:

{
"pageInfo": {
"totalCount": 1,
"page": 0,
"pageSize": 1000
},
...
"certificateSignatureInfo": [
{
"id": "1ec4d004-35fa-4689-b5d6-485c031250b1",
"applianceHostname": "sfo-m01-vc01.sfo.rainpole.io",
"commonName": "sfo-m01-vc01.sfo.rainpole.io",
"csr": "-----BEGIN CERTIFICATE REQUEST----- MIIDNzCCAh8CAQAwgYsxJTAjBgNVBAMMHHNmby1tMDEtdmMwMS5zZm8ucmFpbnBv bGUuaW8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQH DAlQYWxvIEFsdG8xDzANBgNVBAoMBlZNd2FyZTEbMBkGA1UECwwSVk13YXJlIEVu Z2luZWVyaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtz1lnEC/ Cla8RW+XL65Ft+v11dZzWGHHStwK4XY5Dh4nFSqT3os1Pcp21CCK0c2ZoQ7LcOMM uJlolGN4FvAg7h93g26GebKNPEAqS+kxgY63Yvwz3GU6T2Vff/nN2rAEN0k2HEhU CRfl1hhB5Qt849sOHa7mBH7bfhaFrtrXZttB8y41odyHeImGrpfZiK9RpKPYFOqz gRGlHJbKav6V+OJr+tyl4ktK1ItFy0KGyX6k4ZozmWFdhtJ7W8kf/kz65oqfvz4y RSapaje7R6kgeHM9LNzUDxyOow4Bhi1Cccnvwwc1x2vErnCgvAhtI8vGUl05E/oP DIa9z/iJDx0ewQIDAQABoGYwZAYJKoZIhvcNAQkOMVcwVTALBgNVHQ8EBAMCBeAw JwYDVR0RBCAwHoIcc2ZvLW0wMS12YzAxLnNmby5yYWlucG9sZS5pbzAdBgNVHQ4E FgQUALBw3RksE0RTJaE7o84SqvuiLlAwDQYJKoZIhvcNAQELBQADggEBABdQAYzs z+xh2cwNLiYqM4qm95y4FGfdPeEyl0P2C+gig7nklMyTsHOZolxnXo0S2biPpN/5 B8uF50RZGmQtMWUf28JzPWv5QSn68gvBJ+ItzGiK3MMaOzYamzFze32brljYJwIP QU140WY7aUnnYbpQ2MDxAyEr1gZ9ukqXydyy64EzFnj0PSX4LWHu8iJStctI5BnP wQpaj/q8YuDzsLiW6G7ZlgNRF9yW7+y7LQYOnznP4+OwX8EwlG4Zcvydlt7Ys6j4 +MDx5/icu0w8+KqvbzK/wegcz+ct1HtVxQjp3aaJyZSgDBFxbXGbmBNmGUr/xm4m coAVKY2+SoIIWZY= -----END CERTIFICATE REQUEST-----"
}
]
}

Extra the CSR and send it to your CA for signing.

Step 5: Replace the current certificate

Once we have the generated certificate (PEM format), we combine the signed certificate and CA chain into a single file, formatting the contents as a single string to pass into the certificateChain field of the certificate replacement API.

API:

curl --request PUT \
--url https://flt-ops01a.rainpole.io/suite-api/api/fleet-management/certificate-management/certificates/4d5949ec-e922-32f5-8449-818455bf7522 \
--header 'accept: application/json' \
--header 'authorization: Bearer {{apiAccessToken}}' \
--header 'content-type: application/json' \
--data '{
"caType": "EXTERNAL_CA",
"certificateChain": "-----BEGIN CERTIFICATE-----\nMIIFxjCCBK6gAwIBAgITXQAAADTXDd57Ge8msgAAAAAANDANBgkqhkiG9w0BAQsF\nADBLMRUwEwYKCZImiZPyLGQBGRYFbG9jYWwxGDAWBgoJkiaJk/IsZAEZFghyYWlu\ncG9sZTEYMBYGA1UEAxMPcmFpbnBvbGUtREMxLUNBMB4XDTI2MDUxNDEyMzU1MloX\nDTI4MDUxMzEyMzU1MlowgYsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y\nbmlhMRIwEAYDVQQHEwlQYWxvIEFsdG8xDzANBgNVBAoTBlZNd2FyZTEbMBkGA1UE\nCxMSVk13YXJlIEVuZ2luZWVyaW5nMSUwIwYDVQQDExxzZm8tbTAxLXZjMDEuc2Zv\nLnJhaW5wb2xlLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtz1l\nnEC/Cla8RW+XL65Ft+v11dZzWGHHStwK4XY5Dh4nFSqT3os1Pcp21CCK0c2ZoQ7L\ncOMMuJlolGN4FvAg7h93g26GebKNPEAqS+kxgY63Yvwz3GU6T2Vff/nN2rAEN0k2\nHEhUCRfl1hhB5Qt849sOHa7mBH7bfhaFrtrXZttB8y41odyHeImGrpfZiK9RpKPY\nFOqzgRGlHJbKav6V+OJr+tyl4ktK1ItFy0KGyX6k4ZozmWFdhtJ7W8kf/kz65oqf\nvz4yRSapaje7R6kgeHM9LNzUDxyOow4Bhi1Cccnvwwc1x2vErnCgvAhtI8vGUl05\nE/oPDIa9z/iJDx0ewQIDAQABo4ICYDCCAlwwDgYDVR0PAQH/BAQDAgXgMB0GA1Ud\nDgQWBBQAsHDdGSwTRFMloTujzhKq+6IuUDAnBgNVHREEIDAeghxzZm8tbTAxLXZj\nMDEuc2ZvLnJhaW5wb2xlLmlvMB8GA1UdIwQYMBaAFCq42WhbbL7uRa32nuwJM0ge\ntIS7MIHMBgNVHR8EgcQwgcEwgb6ggbuggbiGgbVsZGFwOi8vL0NOPXJhaW5wb2xl\nLURDMS1DQSxDTj1kYzEsQ049Q0RQLENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2Vz\nLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9cmFpbnBvbGUsREM9bG9j\nYWw/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNS\nTERpc3RyaWJ1dGlvblBvaW50MIHEBggrBgEFBQcBAQSBtzCBtDCBsQYIKwYBBQUH\nMAKGgaRsZGFwOi8vL0NOPXJhaW5wb2xlLURDMS1DQSxDTj1BSUEsQ049UHVibGlj\nJTIwS2V5JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixE\nQz1yYWlucG9sZSxEQz1sb2NhbD9jQUNlcnRpZmljYXRlP2Jhc2U/b2JqZWN0Q2xh\nc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTAMBgNVHRMBAf8EAjAAMD0GCSsGAQQB\ngjcVBwQwMC4GJisGAQQBgjcVCIG3rTuF8cRAhMWPLMP3TYL3txiBBIbkuEqB28FM\nAgFkAgERMA0GCSqGSIb3DQEBCwUAA4IBAQBuWF9lTbNWPICFyoE3Yco3Q4aVlctJ\nsTkXnO+5QRoQuEc0fnhFmOqo993HF6Ywc6YWDfrmTQlvQchc0pvljuy+cDc22izQ\nBugZ3WwC+DJ/5tsqFZdLN0wbW1E+fUyaaRfhqls5zBAMLaGrmDZ0zx9a3orynwR+\nCGzbF+6fD7QR+G94oW+YLJKfJgtJKQvN7csVAwKzf4hnsoQQ+kdccAPF9KaTGJXF\n96+SfgpDitxueNziGYJLNjlDtuQciDNQa2uUdZqf2iTF1SrxX/78jPL9ma+XrkZt\n/r/ayHb6Tmenoyf14H48Y7eB+sqmZoQ2ElcCQADwLXjzXyMCfqI2K1qX\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDcTCCAlmgAwIBAgIQTpBdZRYMtINF6R3tDpurszANBgkqhkiG9w0BAQsFADBL\nMRUwEwYKCZImiZPyLGQBGRYFbG9jYWwxGDAWBgoJkiaJk/IsZAEZFghyYWlucG9s\nZTEYMBYGA1UEAxMPcmFpbnBvbGUtREMxLUNBMB4XDTI2MDEwMzExMDg1MVoXDTM2\nMDEwMzExMTg1MVowSzEVMBMGCgmSJomT8ixkARkWBWxvY2FsMRgwFgYKCZImiZPy\nLGQBGRYIcmFpbnBvbGUxGDAWBgNVBAMTD3JhaW5wb2xlLURDMS1DQTCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMMutne9ADaJrNCCrRiHlDcdPNIQtods\nhQuDfxn2jXul1DHLQkERjKy9oobWqiUFeXT5ZJiTFDXuQkA8ZOxErSC24fWSWwSv\nVQOHTRAAdUHyLWlXue1DjQtWH0GJaayE0mHmJBb4kO02naCP5NpvFST4w+7pUkF+\nBrC1CMImPUy6cl3WJiaOEpIzb6NCoaan6uV01/nzuBJg0Acj9pVPE8C2gvqd1Y92\nAhaxMFlNgYdJINXpUu6BdlPmRpIXBJqBa6QBeKeogd4mz6wN6dUnj7nIxDZ+vBDB\nvEt8N4nwqh+HchTyv/ulA5enJA4uXAGJaIhYPtwKRdG0RrrLkz27EQkCAwEAAaNR\nME8wCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCq42Whb\nbL7uRa32nuwJM0getIS7MBAGCSsGAQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBCwUA\nA4IBAQCoHNaWa0Mi7THIheQjNZn9xG1YfkiLz3SM5gkLKvKCob4e1qJJPUd9hewU\n0yV9BKbDjVZODXvtgI+1PiRE2T5BbjPI5FRXjAYLV2ieg6EpXjgWBTQY19H3KjOG\nORCLZIHtj2tpGTOHLWFEPXndCwfeUGwDIL1kcTwvccj95+ojXvxC9c1czNxW0r7B\n/G6jf4XiWTzzCurUduliMg5JZq7PZfIhfErnBUYLB17hDhRIKycTn7WRB1pyCsSD\n1ywzgajgwU3jpF3lDlZnIYyI2E+QP4AWICzc7nM4cnaaanFS0rZU8s9IGedOWD3g\nXvJw0FTstV//2f0INW7+ZoOPSARr\n-----END CERTIFICATE-----\n"
}'

Response:

{
"requestId": "cc784f1c-797e-4de7-ab28-393d3d1382fe",
"requestName": "ucmreplacecertificateworkflow",
"requestReason": "Certificate replace operation from API",
"requestType": "Replace Certificate",
"state": "INPROGRESS",
"errorCause": [],
"duration": 159,
"category": "VCF_CERTIFICATE_MANAGEMENT"
}

With the returned requestId, we can use the same task status-check API as Step 3 to verify the progress. As shown below, the API response confirms that the certificate replacement has completed successfully.

{
"requestId": "cc784f1c-797e-4de7-ab28-393d3d1382fe",
"requestName": "ucmreplacecertificateworkflow",
"requestReason": "Certificate replace operation from API",
"requestType": "Replace Certificate",
"state": "COMPLETED",
"errorCause": [],
"duration": 62592,
"category": "VCF_CERTIFICATE_MANAGEMENT"
}

Leave a comment