Set up a health check for your private CA hierarchy and revocation services
27 minute read
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 on demand.
Before you begin
Before you begin, make sure you have:
An API token with the following permissions:
VIEW_CM_CA- List CAs and download CA certificatesVIEW_CM_CA_CONFIGURATION- View CRLsVIEW_CM_OCSP_RESPONDER- View OCSP responders (optional step only)
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.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.
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 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
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 -X GET "https://demo.one.digicert.com/certificate-authority/api/v1/ca?offset=0&limit=100" \
-H "x-api-key: SERVICE_API_TOKEN" | jq '.'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())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<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Status Code: " + response.statusCode());
System.out.println(response.body());
}
}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):
{
"total": 1,
"offset": 0,
"limit": 100,
"items": [
{
"id": "<ca-id>",
"name": "<ca-name>",
"cert_type": "intermediate",
"key_type": "rsa",
"hosted_type": "online",
"status": "active",
"issuer": {
"id": "<issuing-ca-id>"
},
"validity": {
"valid_to": "<YYYY-MM-DDThh:mm:ssZ>"
},
"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
statusispending_signhas novalidityobject, because it has no signed certificate yet. Skip the validity check for any CA withoutvalidity. - Recertifying a CA disables the original CA, so a
disabledCA is usually expected. ReportdisabledCAs as informational, and skip their CRLs. - A CA whose
hosted_typeisexternalorthird_partyhas 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_typevalues can differ in case, such asrsaandRSA. 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. You download each CA certificate by id when you probe what relying parties receive.
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 -X GET "https://demo.one.digicert.com/certificate-authority/api/v1/crl?offset=0&limit=100" \
-H "x-api-key: SERVICE_API_TOKEN" | jq '.'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())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<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Status Code: " + response.statusCode());
System.out.println(response.body());
}
}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):
{
"total": 1,
"offset": 0,
"limit": 100,
"items": [
{
"id": "<crl-id>",
"file_name": "<file-name>.crl",
"last_generated_date": "<YYYY-MM-DDThh:mm:ssZ>",
"last_crl_number": 552,
"next_generate_date": "<YYYY-MM-DDThh:mm:ssZ>",
"active": true,
"ca": {
"id": "<ca-id>",
"name": "<ca-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_dateis in the past. - Calculate when it expires as
last_generated_datepluscrl_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 ifgeneration_enabledorpublish_enabledisfalse.
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.
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 -X GET "https://demo.one.digicert.com/certificate-authority/api/v1/ca/CA_ID/download?format=pem" \
-H "x-api-key: SERVICE_API_TOKEN"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)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<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Status Code: " + response.statusCode());
System.out.println(response.body());
}
}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.
Successful response (200 OK):
-----BEGIN CERTIFICATE-----
<base64-encoded-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, Query the OCSP responder, and Check the CA issuers URL use these variables:
PARENT: The certificate of the CA you’re checking, such asparent-ca.pem.CHILD: A certificate that CA issued, such aschild-ca.pemor your monitoring certificate.CHAIN: A file withPARENTand every CA certificate above it, up to the root. To build it, concatenate the certificates you downloaded, following each CA’sissuer.idup 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:
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:
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. 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.
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.
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:
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.
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:
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.
# 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.
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 <runtime-user> and <absolute-path-to-your-health-check> 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.
# CRON TEMPLATE — replace placeholders before installation.
SHELL=/bin/sh
PATH=/usr/bin:/bin
17 */6 * * * <runtime-user> <absolute-path-to-your-health-check>
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.
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. 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 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.
Request:
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 '.'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())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<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Status Code: " + response.statusCode());
System.out.println(response.body());
}
}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):
{
"total": 1,
"offset": 0,
"limit": 500,
"items": [
{
"id": "<responder-id>",
"status": "enabled",
"issuer": {
"id": "<ca-id>"
},
"validity": {
"valid_to": "<YYYY-MM-DDThh:mm:ssZ>"
}
}
]
}
Group the responders by issuer.id, which matches the CA id you saved in 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.
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 | 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 | Investigate the CA. |
| CRL overdue | next_generate_date is in the past | 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 | 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 | 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 | 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 | Check the CA’s responder records in 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 | 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) | 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.
Common errors and solutions
For general API errors, such as authentication failures and rate limits, see the DigiCert API error reference.
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 has passed, and review its newest responder record in 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, 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
/caand/crltotals 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_dayshas a finding, even if itsstatusisactive. 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 APIlast_crl_number. A signature that couldn’t be checked is incomplete. - OCSP results show a verified
goodresponse 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
2and 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 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.