Skip to main content

Add a TLS certificate

You upload a certificate or request one through ACME, then assign it to a site domain. External DNS ACME lets you publish the returned challenge with your DNS provider.

Note: Balancer is in beta. Endpoints, fields and behavior may change before general availability, and the feature must be enabled on your account before use. Contact your NetActuate account manager to request access.

Prerequisites​

  • Balancer access and a key in the BALANCER_API_KEY environment variable with permission for these operations.
  • The API host shown in the Balancer portal. Replace <your-api-host> in every example with that host.
  • A shell, curl and Python 3 for examples that build JSON from environment variables.
  • Control of example.com, an HTTP group and domain, and local PEM certificate and private-key files for the upload option. Set DOMAIN_ID from your domain readback.

Resource identifiers come from your API responses. Set each variable when its value is available, before the first step that uses it.

Environment variableValue
CERTIFICATE_PEMYour local PEM contents.
DOMAIN_IDReturned resource identifier, or your selected scope identifier.
GROUP_IDReturned resource identifier, or your selected scope identifier.
HTTP_LB_IDReturned resource identifier, or your selected scope identifier.
PRIVATE_KEY_PEMYour local PEM contents.
SSL_CERTIFICATE_IDReturned resource identifier, or your selected scope identifier.

For an identifier, enter the value returned for your resource and export it:

read -r -p 'DOMAIN_ID: ' DOMAIN_ID
export DOMAIN_ID

Read the upload files into environment variables before choosing the upload option:

export CERTIFICATE_PEM="$(cat certificate.pem)"
export PRIVATE_KEY_PEM="$(cat private-key.pem)"

Steps​

  1. Choose upload: read your local PEM files and submit them. Set CERTIFICATE_PEM and PRIVATE_KEY_PEM to their contents.

    python3 - <<'PYBODY' | \
    curl --silent --show-error --include \
    --request POST \
    --url "https://<your-api-host>/ssl-certificates" \
    --header "x-api-key: $BALANCER_API_KEY" \
    --header "Content-Type: application/json" \
    --data-binary @-
    import json, os
    print(json.dumps({'domain': 'example.com', 'doLetsEncrypt': False, 'certificate': os.environ['CERTIFICATE_PEM'], 'privateKey': os.environ['PRIVATE_KEY_PEM']}))
    PYBODY

    API reference: Creates an SSL certificate

    Expected: HTTP 200. The following shows the response fields to check; additional data depends on your saved resources. Resource identifiers are illustrative.

    {
    "code": 200,
    "data": {
    "sslCertificateId": 1
    }
    }

    Set SSL_CERTIFICATE_ID to the returned sslCertificateId.

  2. Or choose managed ACME publication instead of upload.

    curl --silent --show-error --include \
    --request POST \
    --url "https://<your-api-host>/ssl-certificates" \
    --header "x-api-key: $BALANCER_API_KEY" \
    --header "Content-Type: application/json" \
    --data-binary '{"domain":"example.com","doLetsEncrypt":true,"dnsChallengeMode":"managed"}'

    API reference: Creates an SSL certificate

    Expected: HTTP 200. The following shows the response fields to check; additional data depends on your saved resources. Resource identifiers are illustrative.

    {
    "code": 200,
    "data": {
    "sslCertificateId": 1
    }
    }

    Set SSL_CERTIFICATE_ID to the returned sslCertificateId.

  3. Or choose external DNS publication instead of either previous option.

    curl --silent --show-error --include \
    --request POST \
    --url "https://<your-api-host>/ssl-certificates" \
    --header "x-api-key: $BALANCER_API_KEY" \
    --header "Content-Type: application/json" \
    --data-binary '{"domain":"example.com","doLetsEncrypt":true,"dnsChallengeMode":"external"}'

    API reference: Creates an SSL certificate

    Expected: HTTP 200. The following shows the response fields to check; additional data depends on your saved resources. Resource identifiers are illustrative.

    {
    "code": 200,
    "data": {
    "sslCertificateId": 1
    }
    }

    Set SSL_CERTIFICATE_ID to the returned sslCertificateId.

  4. Read issuance state. For external mode, publish exactly the returned dnsChallenge.recordName and dnsChallenge.recordValue as a TXT record at your DNS provider.

    curl --silent --show-error --include \
    --request GET \
    --url "https://<your-api-host>/ssl-certificates/$SSL_CERTIFICATE_ID" \
    --header "x-api-key: $BALANCER_API_KEY"

    API reference: Get SSL certificate details

    Expected: HTTP 200. The following shows the response fields to check; additional data depends on your saved resources. Resource identifiers are illustrative.

    {
    "code": 200,
    "data": {
    "sslCertificateId": 1
    }
    }

    Inspect isReady, acmeStatus, failedReason and dnsChallenge. TODO(verify): Provider-specific commands for publishing an external DNS challenge.

  5. After confirming public TXT answers, request another challenge check.

    curl --silent --show-error --include \
    --request POST \
    --url "https://<your-api-host>/ssl-certificates/$SSL_CERTIFICATE_ID/recheck-challenge" \
    --header "x-api-key: $BALANCER_API_KEY"

    API reference: Request an ACME DNS challenge recheck

    Expected: HTTP 200. The following shows the response fields to check; additional data depends on your saved resources. Resource identifiers are illustrative.

    {
    "code": 200,
    "data": {
    "sslCertificateId": 1
    }
    }
  6. When the certificate is ready, assign it to your domain.

    python3 - <<'PYBODY' | \
    curl --silent --show-error --include \
    --request PATCH \
    --url "https://<your-api-host>/http-loadbalancers/$HTTP_LB_ID/groups/$GROUP_ID/domains/$DOMAIN_ID" \
    --header "x-api-key: $BALANCER_API_KEY" \
    --header "Content-Type: application/json" \
    --data-binary @-
    import json, os
    print(json.dumps({'sslCertificateId': int(os.environ['SSL_CERTIFICATE_ID'])}))
    PYBODY

    API reference: Update an HTTP group domain configuration

    Expected: HTTP 200. The following shows the response fields to check; additional data depends on your saved resources. Resource identifiers are illustrative.

    {
    "code": 200,
    "result": "success",
    "data": {
    "domainId": 1
    }
    }
  7. Read the domain assignment.

    curl --silent --show-error --include \
    --request GET \
    --url "https://<your-api-host>/http-loadbalancers/$HTTP_LB_ID/groups/$GROUP_ID/domains/$DOMAIN_ID" \
    --header "x-api-key: $BALANCER_API_KEY"

    API reference: Get HTTP group domain

    Expected: HTTP 200. The following shows the response fields to check; additional data depends on your saved resources. Resource identifiers are illustrative.

    {
    "code": 200,
    "result": "success",
    "data": {
    "domainId": 1
    }
    }

Verify​

Inspect the certificate presented by https://example.com. Confirm its domain coverage and expiry. For external ACME, compare authoritative TXT answers with the exact returned challenge. Certificate readiness and live deployment are separate checks.

Use your configured site domain in this traffic check:

curl --silent --show-error --include "https://example.com/"

Expected: the site behavior described above. The application response body and status depend on your origin and selected policies.

An API mutation saves desired configuration first. Readback and a live traffic check answer different questions. No fixed propagation time is established.

Troubleshooting​

SymptomCauseFix
Challenge remains pendingTXT name/value, delegation, TTL or CAA prevents validationCorrect public DNS and use recheck-challenge.
Wrong certificate is presentedDomain assignment or deployment differsRead the domain certificate assignment and inspect another controlled TLS connection.
Upload is rejectedCertificate and key are not acceptedCheck the PEM material and domain coverage; keep private material out of support messages.

Limits and defaults​

SettingLimit or default
DomainMaximum 255 characters
Certificate nameMaximum 64 characters
ACME defaultdoLetsEncrypt defaults to true
DNS challenge modeDefault managed; managed or external
Issuance and deployment timeNo fixed deadline is established

API status codes​

StatusMeaning
200Request succeeds; inspect returned data.
400Invalid arguments or configuration.
401Authentication or authorization denied.
404Resource or parent is unavailable.
409Resource state conflicts with the operation.
412Inspect the returned error details.
500The request encounters a server error.
  • DELETE /ssl-certificates/{sslCertificateId}
  • GET /http-loadbalancers/{httpLbId}/groups/{groupId}/domains/{domainId}
  • GET /ssl-certificates
  • GET /ssl-certificates/dns-challenges
  • GET /ssl-certificates/{sslCertificateId}
  • PATCH /http-loadbalancers/{httpLbId}/groups/{groupId}/domains/{domainId}
  • PATCH /ssl-certificates/{sslCertificateId}
  • POST /ssl-certificates
  • POST /ssl-certificates/{sslCertificateId}/recheck-challenge
  • POST /ssl-certificates/{sslCertificateId}/renew

API reference​

Need Help?​

Contact your account manager or use the Balancer portal.