--- title: "Set up a health check for your private CA hierarchy and revocation services" description: "Check CA and CRL health through the API, probe published revocation services, and use pseudocode to plan your own scheduled health check.\n" source_url: https://dev.digicert.com/digicert-private-ca-api/tutorials/set-up-health-check-private-ca-hierarchy-revocation-services.html --- Use the DigiCert® Private CA API to check your private CA hierarchy and revocation services. This tutorial is for administrators who want to automate these checks. You will run individual API requests and probes, then use pseudocode to plan your own implementation. Implement and validate that logic in your chosen language before scheduling it. In this tutorial, you will: - List your CAs and check their validity and status. - Check that each CRL is current. - Probe the CRLs, OCSP responders, and CA certificates that relying parties use. - Review pseudocode that combines the checks into an automated workflow. - Learn how to schedule and monitor your implementation. You can also [investigate OCSP responder records](#investigate-ocsp-responder-records-optional) on demand. ## Before you begin Before you begin, make sure you have: - A DigiCert® ONE account with DigiCert Private CA access. - An API token with the following permissions: - `VIEW_CM_CA` - List CAs and download CA certificates - `VIEW_CM_CA_CONFIGURATION` - View CRLs - `VIEW_CM_OCSP_RESPONDER` - View OCSP responders (optional step only) - A dedicated service user for the scheduled job. Every step is read-only, so the service user needs only the view permissions above. To set one up, see [Create and authenticate a service user](https://dev.digicert.com/md/account-manager-api/tutorials/create-and-authenticate-a-service-user.md). - At least one active CA with an active CRL. To check an issuing CA's own CRL and OCSP responder, you also need a valid PEM certificate from each issuing CA you monitor, such as a certificate you keep only for monitoring. To issue one, see [Issue a certificate from a private CA](https://dev.digicert.com/md/digicert-private-ca-api/tutorials/issue-a-certificate-from-a-private-ca.md). Renew it before it expires, so the health check always has a valid certificate to query. - HTTPS access to your tenant's API and network access to the CRL, OCSP, and CA certificate URLs in your certificates, including HTTP on port 80. Run the probes from a network representative of your relying parties. - OpenSSL 3.0 or later, curl, and jq installed. Use `openssl version`, `curl --version`, and `jq --version` to verify. LibreSSL, the default `openssl` on macOS, doesn't support the commands in this tutorial. To verify CRLs signed by ML-DSA CAs, use OpenSSL 3.5 or later. - For automation, a host with the runtime for your chosen implementation and a scheduler. The scheduling example uses Linux cron. Confirm that the cron service is running using your distribution's service manager. The API request tabs include Python with `requests`, Java 11 or later, and .NET 6 or later. Install only the tools for the examples you choose. ## Endpoint overview The main workflow uses the first three endpoints. Responder inventory is an optional investigation. Keep it outside the scheduled run. | Method | Path | Query parameters | Required permission | Used for | |--------|------|------------------|---------------------|----------| | GET | `/ca` | `offset`, `limit` | `VIEW_CM_CA` | CA inventory in Step 1 | | GET | `/crl` | `offset`, `limit` | `VIEW_CM_CA_CONFIGURATION` | CRL inventory in Step 2, and refresh after a newer published CRL in Step 3b | | GET | `/ca/{id}/download` | `format=pem` | `VIEW_CM_CA` | CA certificates in Step 3a | | GET | `/ocsp-responder` | `offset`, `limit` | `VIEW_CM_OCSP_RESPONDER` | Optional responder investigation only | All paths are relative to your tenant's API base URL. The examples use `https://demo.one.digicert.com/certificate-authority/api/v1`. Replace the host for your environment. CRL downloads, OCSP queries, and CA issuers downloads use the URLs embedded in certificates and don't send the API token. For request and response details, see the [DigiCert Private CA API reference](https://dev.digicert.com/md/digicert-private-ca-api/api-reference.md). ## What the health check reports Choose warning windows for CA and CRL expiry. The pseudocode calls these `ca_warning_days` and `crl_warning_hours`. They're suggested configuration names for your implementation, not API fields. DigiCert Private CA doesn't define their values. See [Health check findings](#health-check-findings) for the conditions and remediation. | Result | Meaning | |--------|---------| | `pass` | The check completed and met its criteria. | | `finding` | The check observed a condition that needs attention. | | `informational` | A documented exception or skipped check, such as a disabled CA or unsupported post-quantum (PQC) OCSP query. Review these to understand coverage. | | `incomplete` | The check couldn't reach a conclusion, for example because an API request failed or a monitoring certificate was missing. | A complete run can include informational results. It doesn't certify services that were skipped or URLs that were absent. These checks probe the URLs in child CA certificates and the monitoring certificates you supply. Include certificates representing the URL configurations you need to monitor. ## Step 1: List your CAs and check their validity > **Important** > > A CA reports its `status` independently of its validity period. A CA can report `active` after its `validity.valid_to` date has passed, so the health check compares `valid_to` with the current time instead of relying on `status`. ### CA status values | Value | Meaning | |-------|---------| | `active` | The CA certificate can be used for certificate signing. | | `pending_sign` | The CA certificate is created but not yet signed by its issuing CA. It can't sign certificates. | | `disabled` | The CA certificate can't be used for online certificate signing. | **Request:** **cURL** ```bash curl -X GET "https://demo.one.digicert.com/certificate-authority/api/v1/ca?offset=0&limit=100" \ -H "x-api-key: SERVICE_API_TOKEN" | jq '.' ``` **Python** ```python import requests # Configuration BASE_URL = "https://demo.one.digicert.com" SERVICE_API_TOKEN = "SERVICE_API_TOKEN" # Query parameters OFFSET = "0" LIMIT = "100" response = requests.get( f"{BASE_URL}/certificate-authority/api/v1/ca", headers={"x-api-key": SERVICE_API_TOKEN}, params={"offset": OFFSET, "limit": LIMIT} ) print(f"Status Code: {response.status_code}") print(response.json()) ``` **Java** ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class ApiExample { public static void main(String[] args) throws Exception { // Configuration String baseUrl = "https://demo.one.digicert.com"; String serviceApiToken = "SERVICE_API_TOKEN"; // Query parameters String offset = "0"; String limit = "100"; // Send request HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/certificate-authority/api/v1/ca" + "?offset=" + offset + "&limit=" + limit)) .header("x-api-key", serviceApiToken) .GET() .build(); HttpResponse response = client.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println("Status Code: " + response.statusCode()); System.out.println(response.body()); } } ``` **C#** ```csharp using System; using System.Net.Http; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { // Configuration string baseUrl = "https://demo.one.digicert.com"; string serviceApiToken = "SERVICE_API_TOKEN"; // Query parameters string offset = "0"; string limit = "100"; // Send request using var client = new HttpClient(); client.DefaultRequestHeaders.Add("x-api-key", serviceApiToken); var response = await client.GetAsync($"{baseUrl}/certificate-authority/api/v1/ca?offset={offset}&limit={limit}"); string responseBody = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Status Code: {(int)response.StatusCode}"); Console.WriteLine(responseBody); } } ``` Page through the results with `offset` and `limit` until you have retrieved `total` CAs. Without them, the API returns the first 20 CAs. **Successful response (200 OK):** ```json { "total": 1, "offset": 0, "limit": 100, "items": [ { "id": "", "name": "", "cert_type": "intermediate", "key_type": "rsa", "hosted_type": "online", "status": "active", "issuer": { "id": "" }, "validity": { "valid_to": "" }, "revocation": { "revoked": false } } ] } ``` For each CA in `items`, compare `validity.valid_to` with the current time and your CA window, and check `status` and `revocation.revoked`. Record each finding with the CA `name` and `id`. Some CAs need different handling: - A CA whose `status` is `pending_sign` has no `validity` object, because it has no signed certificate yet. Skip the validity check for any CA without `validity`. - Recertifying a CA disables the original CA, so a `disabled` CA is usually expected. Report `disabled` CAs as informational, and skip their CRLs. - A CA whose `hosted_type` is `external` or `third_party` has a private key that's managed outside DigiCert Private CA. It has no CRLs in DigiCert Private CA, so check only its validity and status. - `key_type` values can differ in case, such as `rsa` and `RSA`. Compare them without regard to case. Save each CA's `id`, `name`, `status`, `cert_type`, `key_type`, `hosted_type`, `issuer.id`, and `validity.valid_to`. A root CA has no `issuer`, and neither does a `pending_sign` CA. For every other CA, `issuer.id` is the `id` of the CA that signed it, so you can build the whole hierarchy from this one response. Use the hierarchy to list the CAs beneath any CA you flag, so your report states the impact of a finding, not just the CA that raised it. You join each CRL's `ca.id` to its CA's `id` when you [check that each CRL is current](#step-2-check-that-each-crl-is-current). You download each CA certificate by `id` when you [probe what relying parties receive](#step-3-probe-what-relying-parties-receive). > **Tip** > > Plan a CA window that allows time to recertify the CA and distribute the new chain to relying parties. It's typically much longer than the window you use for end-entity certificates. ## Step 2: Check that each CRL is current Relying parties that check revocation reject certificates when they can't get a current CRL. DigiCert Private CA regenerates each CRL on a schedule. `crl_frequency_hours` controls how often it regenerates, and `crl_validity_hours` controls how long each CRL remains valid. **Request:** **cURL** ```bash curl -X GET "https://demo.one.digicert.com/certificate-authority/api/v1/crl?offset=0&limit=100" \ -H "x-api-key: SERVICE_API_TOKEN" | jq '.' ``` **Python** ```python import requests # Configuration BASE_URL = "https://demo.one.digicert.com" SERVICE_API_TOKEN = "SERVICE_API_TOKEN" # Query parameters OFFSET = "0" LIMIT = "100" response = requests.get( f"{BASE_URL}/certificate-authority/api/v1/crl", headers={"x-api-key": SERVICE_API_TOKEN}, params={"offset": OFFSET, "limit": LIMIT} ) print(f"Status Code: {response.status_code}") print(response.json()) ``` **Java** ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class ApiExample { public static void main(String[] args) throws Exception { // Configuration String baseUrl = "https://demo.one.digicert.com"; String serviceApiToken = "SERVICE_API_TOKEN"; // Query parameters String offset = "0"; String limit = "100"; // Send request HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/certificate-authority/api/v1/crl" + "?offset=" + offset + "&limit=" + limit)) .header("x-api-key", serviceApiToken) .GET() .build(); HttpResponse response = client.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println("Status Code: " + response.statusCode()); System.out.println(response.body()); } } ``` **C#** ```csharp using System; using System.Net.Http; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { // Configuration string baseUrl = "https://demo.one.digicert.com"; string serviceApiToken = "SERVICE_API_TOKEN"; // Query parameters string offset = "0"; string limit = "100"; // Send request using var client = new HttpClient(); client.DefaultRequestHeaders.Add("x-api-key", serviceApiToken); var response = await client.GetAsync($"{baseUrl}/certificate-authority/api/v1/crl?offset={offset}&limit={limit}"); string responseBody = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Status Code: {(int)response.StatusCode}"); Console.WriteLine(responseBody); } } ``` Page through the results with `offset` and `limit` until you have retrieved `total` CRLs. **Successful response (200 OK):** ```json { "total": 1, "offset": 0, "limit": 100, "items": [ { "id": "", "file_name": ".crl", "last_generated_date": "", "last_crl_number": 552, "next_generate_date": "", "active": true, "ca": { "id": "", "name": "" }, "configuration": { "generation_enabled": true, "publish_enabled": true, "generation": { "crl_validity_hours": 24, "crl_frequency_hours": 12 } } } ] } ``` When you make a new CRL active for a CA, the previously active CRL's `active` changes to `false`. Certificates issued before the change still point to the previous CRL, and DigiCert Private CA still generates it while its `generation_enabled` is `true`. Check every CRL that is `active` or has `generation_enabled` set to `true`, and skip CRLs whose CA is `disabled`. For each CRL you check: - Flag it as overdue if `next_generate_date` is in the past. - Calculate when it expires as `last_generated_date` plus `crl_validity_hours`, and flag it if that time falls inside your CRL window. The published CRL's next update time equals this value. - If the CRL is `active`, flag it if `generation_enabled` or `publish_enabled` is `false`. Include each flagged CRL's `id`, `file_name`, and `ca.name` in the finding so whoever responds can find it. Save each CRL's `ca.id`, `file_name`, and `last_crl_number`. You compare them with the published CRL when you [check the published CRL](#step-3b-check-the-published-crl). > **Note** > > Until you re-enable generation, neither the schedule nor a manual request generates a CRL whose `generation_enabled` is `false`. For the generation and publishing settings, see Create a CRL. ## Step 3: Probe what relying parties receive The API checks report what DigiCert Private CA generates. Relying parties don't read the API. They download CRLs, query OCSP responders, and fetch CA certificates from the URLs inside each certificate. A CA certificate can point to a URL that doesn't serve the CRL the API reports. A responder can fail to answer while the API still reports its CA as `active`. This step runs the same checks a relying party runs. Each CA certificate carries the URLs of the CA that signed it. To check a CA's services, use a certificate that CA issued: - To check a root or other parent CA, use each CA certificate beneath it. - To check an issuing CA, use a valid certificate that the issuing CA issued, such as your monitoring certificate. ### Step 3a: Download the CA certificates Download the certificate of each CA you check and of each CA beneath it. **Request:** **cURL** ```bash curl -X GET "https://demo.one.digicert.com/certificate-authority/api/v1/ca/CA_ID/download?format=pem" \ -H "x-api-key: SERVICE_API_TOKEN" ``` **Python** ```python import requests # Configuration BASE_URL = "https://demo.one.digicert.com" SERVICE_API_TOKEN = "SERVICE_API_TOKEN" CA_ID = "CA_ID" # Query parameters FORMAT = "pem" response = requests.get( f"{BASE_URL}/certificate-authority/api/v1/ca/{CA_ID}/download", headers={"x-api-key": SERVICE_API_TOKEN}, params={"format": FORMAT} ) print(f"Status Code: {response.status_code}") print(response.text) ``` **Java** ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class ApiExample { public static void main(String[] args) throws Exception { // Configuration String baseUrl = "https://demo.one.digicert.com"; String serviceApiToken = "SERVICE_API_TOKEN"; String caId = "CA_ID"; // Query parameters String format = "pem"; // Send request HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/certificate-authority/api/v1/ca/" + caId + "/download" + "?format=" + format)) .header("x-api-key", serviceApiToken) .GET() .build(); HttpResponse response = client.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println("Status Code: " + response.statusCode()); System.out.println(response.body()); } } ``` **C#** ```csharp using System; using System.Net.Http; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { // Configuration string baseUrl = "https://demo.one.digicert.com"; string serviceApiToken = "SERVICE_API_TOKEN"; string caId = "CA_ID"; // Query parameters string format = "pem"; // Send request using var client = new HttpClient(); client.DefaultRequestHeaders.Add("x-api-key", serviceApiToken); var response = await client.GetAsync($"{baseUrl}/certificate-authority/api/v1/ca/{caId}/download?format={format}"); string responseBody = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Status Code: {(int)response.StatusCode}"); Console.WriteLine(responseBody); } } ``` Replace `CA_ID` with a CA `id` you saved in [List your CAs and check their validity](#step-1-list-your-cas-and-check-their-validity). **Successful response (200 OK):** ```text -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- ``` The response body is the PEM-encoded CA certificate. Save each certificate to its own file. The commands in [Check the published CRL](#step-3b-check-the-published-crl), [Query the OCSP responder](#step-3c-query-the-ocsp-responder), and [Check the CA issuers URL](#step-3d-check-the-ca-issuers-url) use these variables: - `PARENT`: The certificate of the CA you're checking, such as `parent-ca.pem`. - `CHILD`: A certificate that CA issued, such as `child-ca.pem` or your monitoring certificate. - `CHAIN`: A file with `PARENT` and every CA certificate above it, up to the root. To build it, concatenate the certificates you downloaded, following each CA's `issuer.id` up to the root. For a root CA, it's the root certificate alone. For example, in a root → issuing CA → monitoring certificate hierarchy, save the root and issuing CA download responses as `root-ca.pem` and `issuing-ca.pem`. The cURL download request prints PEM. To save the corresponding response, add `--output root-ca.pem` or `--output issuing-ca.pem`. Copy your valid monitoring certificate to `monitoring.pem`. It contains only the public certificate. The health check doesn't need its private key. To check the issuing CA's services, run: ```bash PARENT="issuing-ca.pem" CHILD="monitoring.pem" CHAIN="chain.pem" cat "$PARENT" root-ca.pem > "$CHAIN" ``` To check the root CA's services using the issuing CA certificate, set `PARENT="root-ca.pem"`, `CHILD="issuing-ca.pem"`, and `CHAIN="root-ca.pem"`, then repeat Steps 3b–3d. For deeper hierarchies, include every ancestor in `CHAIN`. Your automated implementation needs to download and assemble these CA files for each probe. A certificate might not include every URL. For example, certificates issued by an offline CA might have no CRL distribution point, OCSP, or CA issuers URL. Each command runs once for every URL of its kind in `CHILD`, and does nothing when there are none. Report a certificate without a URL for a probe as informational. ### Step 3b: Check the published CRL Download the CRL from each CRL location in the certificate and check its signature and next update time. **Command:** ```bash openssl x509 -in "${CHILD}" -noout -ext crlDistributionPoints | sed -n 's/.*URI://p' > cdp-urls.txt while read -r CDP_URL; do case "${CDP_URL}" in http://*) ;; *) echo "Not published automatically: ${CDP_URL}"; continue ;; esac curl -sS -f -o published.crl "${CDP_URL}" || continue openssl crl -inform DER -in published.crl -CAfile "${CHAIN}" -noout -nextupdate -crlnumber done < cdp-urls.txt ``` DigiCert Private CA publishes CRLs automatically only to HTTP locations. For an HTTPS, LDAP, or file location, it generates the CRL but doesn't publish it, so the loop reports the location instead of downloading it. Report each one so whoever publishes the CRL there can confirm it's current. For each HTTP location, `curl -f` fails when the URL doesn't return the CRL. `openssl crl` prints `verify OK` when the CA's key signed the CRL, followed by `nextUpdate` and `crlNumber`. Raise a finding if the URL returns an HTTP error, the signature doesn't verify, or `nextUpdate` falls inside your CRL window. Record connection failures and timeouts as incomplete checks. Then find the matching CRL among those you saved when you [checked that each CRL is current](#step-2-check-that-each-crl-is-current). Its `ca.id` is the parent CA's `id`, and its `file_name` matches the file name at the end of the URL. Its `last_crl_number` must equal `crlNumber`. OpenSSL prints `crlNumber` in hexadecimal, for example `0x0228` for `552`. If `crlNumber` is greater than `last_crl_number`, DigiCert Private CA probably generated a new CRL after you listed the CRLs. List them again before you raise a finding. If no CRL matches, or the numbers still differ, the certificate points relying parties to a different CRL than the one DigiCert Private CA generates. When a CRL uses automatic partitioning, DigiCert Private CA appends a partition identifier to its file name, so the file name in the URL starts with `file_name` instead of equaling it. > **Note** > > If OpenSSL doesn't recognize the CA's key algorithm, `openssl crl -CAfile` prints `Error getting CRL issuer public key` and a `decode error`. This happens for CAs whose certificates use a pre-standard algorithm identifier, such as early SLH-DSA CAs. For those CAs, run the `openssl crl` command without `-CAfile` to read `nextUpdate` and `crlNumber`, and record that the signature wasn't verified. ### Step 3c: Query the OCSP responder Ask the CA's OCSP responder for the status of the child certificate. > **Important** > > ML-DSA and SLH-DSA (post-quantum) certificates don't support OCSP checking yet. Skip the OCSP query when the parent CA's `key_type` is `MLDSA` or `SLHDSA`, or when the child certificate uses an ML-DSA or SLH-DSA key. To check the child's key algorithm, run `openssl x509 -in "${CHILD}" -noout -text` and read `Public Key Algorithm`. Still check their CRLs. **Command:** ```bash for OCSP_URL in $(openssl x509 -in "${CHILD}" -noout -ocsp_uri); do openssl ocsp -issuer "${PARENT}" -cert "${CHILD}" -url "${OCSP_URL}" -CAfile "${CHAIN}" -timeout 20 done ``` For a healthy responder, `openssl ocsp` prints `Response verify OK` and a status of `good`, followed by `This Update` and `Next Update` times. > **Note** > > The responder doesn't return a nonce, so OpenSSL also prints `WARNING: no nonce in response`. This warning doesn't indicate a failure. Raise a finding if the response doesn't verify, the status isn't `good`, or the responder returns an error such as `unauthorized (6)`. The responder returns `unauthorized` when it has no valid answer for the certificate, for example because the CA has expired. ### Step 3d: Check the CA issuers URL Relying parties that lack an intermediate certificate download it from the CA issuers URL. Confirm that the URL returns the parent CA's certificate. **Command:** ```bash openssl x509 -in "${PARENT}" -noout -fingerprint -sha256 for AIA_URL in $(openssl x509 -in "${CHILD}" -noout -ext authorityInfoAccess | sed -n 's/.*CA Issuers - URI://p'); do curl -sS -f -o published-issuer.crt "${AIA_URL}" || continue openssl x509 -inform DER -in published-issuer.crt -noout -fingerprint -sha256 done ``` The first line prints the parent's fingerprint. Each fingerprint the loop prints must match it. Raise a finding if a URL returns an HTTP error, the downloaded certificate is invalid, or a fingerprint differs. Record connection failures and timeouts as incomplete checks. ## Step 4: Plan the automated health check The following **pseudocode** shows one way to combine Steps 1–3. It isn't executable code. The operations described in it represent logic you need to implement in your chosen language, including API requests, certificate handling, reporting, and error handling. Supply your API base URL, a protected source for the service user's token, your warning windows, and a valid monitoring certificate for each issuing CA you check. Use the response fields and probe conditions from the preceding steps. ```text # PSEUDOCODE — illustrates control flow; cannot be run as a script. function health_check(): load API base URL and token from protected configuration load ca_warning_days, crl_warning_hours, and monitoring certificates initialize results and a run deadline acquire a lock to prevent overlapping runs if the lock is unavailable: report that this run could not start exit 2 try: cas = list_all_pages("/ca") crls = list_all_pages("/crl") build the CA hierarchy using each CA's id and issuer.id for each ca in cas: check status and revocation.revoked as described in Step 1 if validity exists: compare validity.valid_to with now and ca_warning_days else: record that the validity check was skipped include affected descendant CA IDs with each CA finding for each crl in crls: find its CA by matching crl.ca.id to ca.id skip CRLs whose CA is disabled if crl.active or crl.configuration.generation_enabled: compare next_generate_date with now crl_validity_hours = crl.configuration.generation.crl_validity_hours calculate expiry from last_generated_date + crl_validity_hours compare expiry with now and crl_warning_hours if crl.active: check generation_enabled and publish_enabled record results with the CRL id, file_name, and CA name for each CA whose published services are in scope: obtain certificates that this CA issued: child CA certificates for parent CA checks valid monitoring certificates for issuing CA checks if a required certificate is missing or invalid: record incomplete coverage for this CA continue with the next CA download the CA certificates with GET /ca/{id}/download?format=pem prepare PARENT, CHILD, and CHAIN as described in Step 3a for each CHILD certificate: inspect its CRL, OCSP, and CA issuers URLs record absent URLs as informational check each published CRL as described in Step 3b if its crlNumber is newer than the API's last_crl_number: refresh the CRL inventory before reporting a mismatch query each supported OCSP URL as described in Step 3c record unsupported PQC OCSP checks as informational compare each CA issuers download with PARENT as in Step 3d group dependent CRL and OCSP findings beneath expired-CA findings on a required operation that cannot complete: record an incomplete result and retain findings already collected finally: write a report with UTC timestamps, resource IDs, and check results if the report cannot be saved: mark this run incomplete if the report was saved and no required check is incomplete: record the time of this completed run remove temporary certificate and CRL files release the lock if any required check is incomplete or the report could not be saved: exit 2 if any findings need attention: exit 1 exit 0 function list_all_pages(path): offset = 0 items = [] repeat within the run deadline: page = GET path with offset and limit require a valid response and forward progress append page.items to items offset = offset + count(page.items) until count(items) equals page.total return items ``` Apply the scope rules in Steps 1–3 when implementing these operations. In particular, skip service probes for CAs without a signed certificate and for `disabled`, `external`, or `third_party` CAs. Record the reason for each skipped check so readers of the report can assess coverage. Keep the optional responder inventory outside this workflow. Give HTTP requests and OpenSSL commands timeouts, and set an overall run limit. Bound retries for connection failures and `429` or `503` responses, honoring `Retry-After` within that limit. If an operation still can't complete, record it as incomplete. > **Warning** > > Send the API token only to the API. Downloads and probes of certificate URLs don't need it, and those URLs can point to hosts outside your tenant. Use these exit codes to distinguish findings from incomplete runs: | Exit code | Meaning | |-----------|---------| | `0` | Required checks completed with no findings. Review informational results for skipped coverage. | | `1` | Required checks completed with one or more findings. | | `2` | At least one required check couldn't complete, or the report couldn't be saved. Retain findings already collected. | An HTTP error from a published URL or a conclusively invalid signature raises a finding. Connection failures, timeouts, missing inputs, and API failures make the run incomplete. If a CRL signature algorithm is unsupported, record that verification is incomplete even if you can read the CRL's dates and number. Before scheduling your implementation, run it manually with the same operating-system account and permissions the scheduled job will use. Review its report and verify the exit code for a completed run, a run with findings, and a run that can't complete. Confirm that the output excludes API tokens and that repeated runs don't change CA resources. ## Step 5: Schedule and monitor your implementation After you have implemented and validated the pseudocode, schedule your script to run more often than the smallest gap between `crl_validity_hours` and `crl_frequency_hours` across the CRLs you monitor. For a frequency of `12` hours and a validity of `24` hours, a six-hour schedule leaves time to detect a missed generation. Keep the overall run limit below the schedule interval. Use a dedicated operating-system account with access to the token and monitoring certificates. This account is separate from the DigiCert service user. Keep the token in a protected file or secret store, and use absolute paths for the executable and its inputs. Cron doesn't load your interactive shell configuration. The following is an **illustrative cron template**, not a command to run as written. Replace `` and `` with the account and executable entry point for your validated implementation. Add any interpreter or configuration arguments it needs. Place the completed entry in a managed file such as `/etc/cron.d/pca-health-check`. This system crontab format includes the username. ```text # CRON TEMPLATE — replace placeholders before installation. SHELL=/bin/sh PATH=/usr/bin:/bin 17 */6 * * * ``` This schedule uses the host's time zone. On a UTC host, it runs at 17 minutes past midnight, six a.m., noon, and six p.m. Confirm the host's time zone and clock synchronization, and route the program's output to your logging system with an appropriate retention policy. > **Important** > > Cron starts the process but doesn't alert on its exit code. Connect your implementation's results to your monitoring system, or failed runs go unnoticed. Alert on findings according to your warning policy, incomplete runs, and a missing completed run. Record completion time for both exit `0` and exit `1`. A completed run with findings still proves the check ran. An incomplete run must not advance that timestamp. For a six-hour schedule with a 20-minute run limit, seven hours is an example deadline for a missing-run alert. Monitor the host or use an external heartbeat check so a stopped host or cron service is also detected. Confirm that the first scheduled run produces a report at the expected time. Rotate the API token before expiry, as described in [Manage API token lifecycle](https://dev.digicert.com/md/account-manager-api/tutorials/manage-api-token-lifecycle.md). To stop the job, remove its cron entry and retain its reports according to your log retention policy. ## Investigate OCSP responder records (optional) Run this investigation on demand. [Query the OCSP responder](#step-3c-query-the-ocsp-responder) tests each responder directly, which is the check that matters to relying parties. The responder records add context when you investigate an OCSP finding, but listing them is slow. > **Note** > > Each record includes the responder certificate, and responders rotate automatically, so the list keeps growing. The API has no filter for this list, so every run pages through all of it. In testing, paging more than 6,000 responders at 500 per page took about three minutes. In an environment with more than 500,000 responders, a single page took over 100 seconds. **Request:** **cURL** ```bash curl -X GET "https://demo.one.digicert.com/certificate-authority/api/v1/ocsp-responder?offset=0&limit=500" \ -H "x-api-key: SERVICE_API_TOKEN" | jq '.' ``` **Python** ```python import requests # Configuration BASE_URL = "https://demo.one.digicert.com" SERVICE_API_TOKEN = "SERVICE_API_TOKEN" # Query parameters OFFSET = "0" LIMIT = "500" response = requests.get( f"{BASE_URL}/certificate-authority/api/v1/ocsp-responder", headers={"x-api-key": SERVICE_API_TOKEN}, params={"offset": OFFSET, "limit": LIMIT} ) print(f"Status Code: {response.status_code}") print(response.json()) ``` **Java** ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class ApiExample { public static void main(String[] args) throws Exception { // Configuration String baseUrl = "https://demo.one.digicert.com"; String serviceApiToken = "SERVICE_API_TOKEN"; // Query parameters String offset = "0"; String limit = "500"; // Send request HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/certificate-authority/api/v1/ocsp-responder" + "?offset=" + offset + "&limit=" + limit)) .header("x-api-key", serviceApiToken) .GET() .build(); HttpResponse response = client.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println("Status Code: " + response.statusCode()); System.out.println(response.body()); } } ``` **C#** ```csharp using System; using System.Net.Http; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { // Configuration string baseUrl = "https://demo.one.digicert.com"; string serviceApiToken = "SERVICE_API_TOKEN"; // Query parameters string offset = "0"; string limit = "500"; // Send request using var client = new HttpClient(); client.DefaultRequestHeaders.Add("x-api-key", serviceApiToken); var response = await client.GetAsync($"{baseUrl}/certificate-authority/api/v1/ocsp-responder?offset={offset}&limit={limit}"); string responseBody = await response.Content.ReadAsStringAsync(); Console.WriteLine($"Status Code: {(int)response.StatusCode}"); Console.WriteLine(responseBody); } } ``` Page through the results with `offset` and `limit` until you have retrieved `total` responders. **Successful response (200 OK):** ```json { "total": 1, "offset": 0, "limit": 500, "items": [ { "id": "", "status": "enabled", "issuer": { "id": "" }, "validity": { "valid_to": "" } } ] } ``` Group the responders by `issuer.id`, which matches the CA `id` you saved in [List your CAs and check their validity](#step-1-list-your-cas-and-check-their-validity). For each CA, keep the responder with the latest `validity.valid_to`. Flag an online, non-PQC CA if it has no responder, or if its newest responder's `valid_to` is in the past. An expired responder can still report a `status` of `enabled`, so check `validity.valid_to` instead of `status`. > **Note** > > A CA whose `hosted_type` is `offline` might have no OCSP responder by design. Report offline CAs without a responder as informational, unless your policy requires OCSP for them. ## Health check findings Apply these conditions to the resources in scope. The pseudocode uses `ca_warning_days` and `crl_warning_hours` for your warning windows. The responder-record check is available only in the optional investigation. | Check | Condition | Source | Remediation | |-------|-----------|--------|-------------| | CA expiry | `validity.valid_to` is in the past or inside your CA window | [List your CAs and check their validity](#step-1-list-your-cas-and-check-their-validity) | Recertify the CA. | | CA status | `status` is neither `active` nor `disabled`, or `revocation.revoked` is `true`. A `disabled` status alone is informational. | [List your CAs and check their validity](#step-1-list-your-cas-and-check-their-validity) | Investigate the CA. | | CRL overdue | `next_generate_date` is in the past | [Check that each CRL is current](#step-2-check-that-each-crl-is-current) | Generate the CRL manually and investigate the missed schedule. | | CRL near expiry | `last_generated_date` plus `crl_validity_hours` falls inside your CRL window | [Check that each CRL is current](#step-2-check-that-each-crl-is-current) | Generate the CRL manually and investigate the missed schedule. | | CRL disabled | `generation_enabled` or `publish_enabled` is `false` on an active CRL | [Check that each CRL is current](#step-2-check-that-each-crl-is-current) | Re-enable generation or publishing. | | Published CRL | The CRL URL returns an HTTP error or invalid CRL, its signature doesn't verify, its next update falls inside your CRL window, or its CRL number has no matching API record | [Check the published CRL](#step-3b-check-the-published-crl) | Compare the URL with the CRL's `file_name`. A wrong URL in a CA certificate needs a new CA certificate. | | OCSP response | The responder returns an error, the response doesn't verify, or the status isn't `good` | [Query the OCSP responder](#step-3c-query-the-ocsp-responder) | Check the CA's responder records in [Investigate OCSP responder records (optional)](#investigate-ocsp-responder-records-optional). | | CA certificate URL | The CA issuers URL returns an HTTP error or a certificate that doesn't match the parent | [Check the CA issuers URL](#step-3d-check-the-ca-issuers-url) | Confirm the URL in the CA certificate. | | Responder records (on demand) | An online CA without a PQC key type has no unexpired responder | [Investigate OCSP responder records (optional)](#investigate-ocsp-responder-records-optional) | Check responder generation for the CA. | When a CA is expired, report its CRL and OCSP findings under the CA expiry finding instead of as separate findings. Relying parties can't validate a CRL or OCSP response that chains to an expired CA, so those findings repeat the same root cause. > **Important** > > DigiCert Private CA keeps generating CRLs on schedule for an expired CA, so its CRLs look current in the API. Check the CA's expiry, not only its CRLs. ## Common errors and solutions For general API errors, such as authentication failures and rate limits, see the [DigiCert API error reference](https://dev.digicert.com/md/get-started/error-handling-rate-limits.md). ### The OCSP query returns unauthorized `openssl ocsp` prints `Responder Error: unauthorized (6)`. The responder has no valid answer for the certificate. Check whether the CA's `validity.valid_to` from [List your CAs and check their validity](#step-1-list-your-cas-and-check-their-validity) has passed, and review its newest responder record in [Investigate OCSP responder records (optional)](#investigate-ocsp-responder-records-optional). The same error appears when you query a serial number the CA never issued, so always query a real certificate. ### The published CRL doesn't match the API The URL in the certificate returns `404 Not Found` or a CRL that doesn't match the API. Its `crlNumber` or next update time differs from the values you saved when you [checked that each CRL is current](#step-2-check-that-each-crl-is-current), even after you list the CRLs again. The CA certificate points relying parties to another location, for example a host in a different DigiCert ONE environment. Relying parties use the URL in the certificate, so they depend on whatever that location serves. Plan a new CA certificate with the correct URLs. ## Verify the results Verify the individual checks and, after implementing the pseudocode, your automation: - The report's `/ca` and `/crl` totals cover all returned inventory pages. Every accessible CA has a validity or status result, including an explicit reason when validity is unavailable. - Each expired CA or CA inside `ca_warning_days` has a finding, even if its `status` is `active`. Its finding lists descendant CA IDs. - Each CRL in scope has a freshness result. Findings identify the CRL and its CA. Disabled-CA and inactive-CRL exclusions are informational. - Published CRL results cover the signature, `nextUpdate`, and matching API `last_crl_number`. A signature that couldn't be checked is incomplete. - OCSP results show a verified `good` response or a finding. PQC exclusions and absent URLs are explicit informational results. - CA issuers URL results show a matching parent fingerprint or a finding. - Your implementation produces a readable report and the documented exit code. An unavailable API produces exit `2` and leaves the last completed-run timestamp unchanged. - The first scheduled run produces a report at the expected time. Your monitoring system receives a controlled finding and an incomplete-run alert, and detects a missing completed run. Validate these cases in a separate test environment. Review informational results before declaring coverage sufficient for your environment. The health check samples certificate URLs. It doesn't inspect every issued certificate. ## Next steps After you implement and validate your health check, you can: - **Replace certificates from a flagged CA**: Use [Issue a certificate from a private CA](https://dev.digicert.com/md/digicert-private-ca-api/tutorials/issue-a-certificate-from-a-private-ca.md) to issue replacements from a healthy CA as part of your lifecycle automation. - **Run the check after every CA change**: Trigger the same script from your change pipeline after you create, recertify, or reconfigure a CA. The run catches a misconfigured CRL or responder before the next scheduled check.