Phase 1
Establish the account foundation
Identify the account and organization and create the setup service identity and credentials.
Phase details
- Workstream
- Shared foundation · Step 1 of 2
- Product
- Account Manager
- Time
- 25 minutes
- Setup
- One-time setup
- Goal
- Create a service user with limited roles, capture its API token, and create its client authentication certificate.
- Inputs
bootstrap_api_tokenaccount_nameorganization_nameclient_auth_certificate_expiration- Expected outputs
account_idorganization_idservice_user_idapi_token_idservice_api_tokenclient_auth_certificate_idclient.crtclient.key
Identify the target account, organization, and required tenant roles. Then create the setup service user and both authentication factors required later by DigiCert® Software Trust Manager.
Phase prerequisites
Make sure you have:
- A bootstrap user with an API token for the target tenant, the Manage users permission in DigiCert® Account Manager, and access to the target account, organization, and assignable roles. Make the token available as
BOOTSTRAP_API_TOKENfor cURL orbootstrap_api_tokenfor Python. - The exact target account and organization names.
- Authorization to assign the tenant roles that provide the capabilities listed in Before you begin. You select their exact names in this phase.
- A secure destination for the service-user API token, client certificate, and private key.
- An approved UTC expiration timestamp for the client authentication credential.
Endpoints used
| Method | Path | Purpose |
|---|---|---|
| GET | /account/api/v1/account | List accessible accounts. |
| GET | /account/api/v1/role | List assignable roles by product. |
| GET | /account/api/v1/organization | List organizations in the target account. |
| POST | /account/api/v1/user | Create the service user and return its API token once. |
| GET | /account/api/v1/user/{user_id} | Verify assigned roles and effective permissions. |
| POST | /account/api/v1/client-auth-certificate | Sign a certificate signing request (CSR) and create a client authentication certificate for the authenticated service user. |
| GET | /account/api/v1/user/me on the clientauth. host | Verify the new certificate authenticates as the service user. |
Step 1.1: Select the target account
GET /account/api/v1/account returns an array. Select the intended active account explicitly. Do not assume the response contains only one account.
ACCOUNT_NAME="Example account"
ACCOUNT_ID="$(
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/account/api/v1/account" \
-H "x-api-key: ${BOOTSTRAP_API_TOKEN}" |
jq -er --arg name "${ACCOUNT_NAME}" '
[.[] | select(.name == $name and .active == true)] |
if length == 1 then .[0].id
else error("expected one active target account")
end'
)"
export ACCOUNT_ID
printf '%s\n' "${ACCOUNT_ID}" > account-id.txt
printf 'Selected account: %s (%s)\n' "${ACCOUNT_NAME}" "${ACCOUNT_ID}"import pathlib
import requests
base_url = "https://demo.one.digicert.com"
account_name = "Example account"
response = requests.get(
f"{base_url}/account/api/v1/account",
headers={"x-api-key": bootstrap_api_token},
timeout=30,
)
response.raise_for_status()
accounts = response.json()
matches = [item for item in accounts if item["name"] == account_name and item["active"]]
if len(matches) != 1:
raise RuntimeError(f"Expected one active target account. Found {len(matches)}")
account_id = matches[0]["id"]
pathlib.Path("account-id.txt").write_text(f"{account_id}\n")
print(f"Selected account: {matches[0]['name']} ({account_id})")The cURL example exports ACCOUNT_ID. The Python example assigns account_id.
Step 1.2: Find the required role names
Retrieve the tenant’s role names, then select the minimum set that grants the DigiCert® Private CA, DigiCert® Device Trust Manager, and Software Trust Manager operations listed in Before you begin. Role names vary by tenant.
GET /account/api/v1/role returns an object keyed by product code, where each value is an array of role objects. The keys this workflow needs are ca_manager for DigiCert Private CA, device_trust_manager for Device Trust Manager, and secure_software_manager for Software Trust Manager. Request every key rather than filtering with application_code, because the documented application_code values do not include device_trust_manager.
Retrieve the role inventory:
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/account/api/v1/role?account_id=${ACCOUNT_ID}" \
-H "x-api-key: ${BOOTSTRAP_API_TOKEN}" \
-o role-inventory.json
Confirm that the response contains roles for every product in the workflow. Stop if any product is missing:
jq -er '
. as $roles |
["ca_manager", "device_trust_manager", "secure_software_manager"] |
map(select((($roles[.] // []) | length) == 0)) as $missing |
if ($missing | length) == 0 then true
else error("no roles returned for: " + ($missing | join(", ")) +
". Confirm the account is entitled to the product and that the bootstrap user can view its roles")
end' role-inventory.json > /dev/null
Inspect the active candidates. You use their exact name values when you create the service user:
jq '{ca_manager, device_trust_manager, secure_software_manager} |
map_values([.[] | select(.status == "ACTIVE") | {name, display_name, description, type}])' \
role-inventory.json
The projection omits archived roles because you cannot assign them. Each role object in the full response has this shape:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "CM_PKI_MANAGER",
"display_name": "CM PKI MANAGER",
"description": "Role with CM view permissions",
"type": "default",
"status": "ACTIVE",
"access_scope": "account"
}
A role object does not list the permissions it grants. Use display_name and description to choose candidate roles for each product, record their exact name values, and confirm the permissions they actually grant when you create the setup service user. That step returns the effective permission codes and fails if a required one is missing.
Prefer type: default roles that match one capability row. Use a broad administrator role only as a last resort. A broader role creates an over-privileged credential even when the workflow succeeds.
Do not copy example role names from the API reference or another account. The names above are illustrative, and custom roles differ between tenants.
Step 1.3: Select an organization
Software Trust Manager private trust certificate profiles associate issued certificates with an organization. Select the intended active organization by exact name.
ORGANIZATION_NAME="Example organization"
ORGANIZATION_ID="$(
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/account/api/v1/organization?account_id=${ACCOUNT_ID}" \
-H "x-api-key: ${BOOTSTRAP_API_TOKEN}" |
jq -er --arg name "${ORGANIZATION_NAME}" '
[.[] | select(.name == $name and .active == true)] |
if length == 1 then .[0].id
else error("expected one active target organization")
end'
)"
export ORGANIZATION_ID
printf '%s\n' "${ORGANIZATION_ID}" > organization-id.txt
printf 'Selected organization: %s (%s)\n' "${ORGANIZATION_NAME}" "${ORGANIZATION_ID}"
The example fails unless the approved name identifies exactly one active organization. It exports ORGANIZATION_ID as the shell equivalent of organization_id.
Step 1.4: Create the setup service user
Restrict access to files created in the current shell. umask 077 gives only the current user permissions on new files unless a command requests a more restrictive mode:
umask 077
Set ROLE_NAMES_JSON to an array of exact role name values from Find the required role names. The roles property takes role names, not role IDs:
ROLE_NAMES_JSON='[
"<private-ca-role-name>",
"<device-trust-role-name>",
"<software-trust-role-name>"
]'
Replace each role-name placeholder with the exact value you selected. Then create the service user. The API returns api_token.token only once, so save the response to the protected working directory and do not print it in logs:
jq -n \
--arg account_id "${ACCOUNT_ID}" \
--argjson roles "${ROLE_NAMES_JSON}" \
'{
user_type: "service",
friendly_name: "Private trust stack setup",
email: "pki-automation-owner@example.com",
description: "Setup identity for the private trust solution",
accounts: [$account_id],
roles: $roles
}' |
curl --fail-with-body --silent --show-error \
-X POST "https://demo.one.digicert.com/account/api/v1/user" \
-H "x-api-key: ${BOOTSTRAP_API_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @- \
-o service-user-response.json
This create operation can return 200 or 201. curl --fail-with-body accepts either successful 2xx result. Confirm success from the required response fields and the retrieved user record, not one exact 2xx code. If the result is non-2xx or uncertain, do not repeat the POST until you list service users for the target account and check for the exact friendly_name. A response-handling failure does not prove that the user was not created.
A successful response contains the service user id, an api_token object, and an applications array with the permissions granted by the assigned roles:
{
"id": "5e0bd5fe-117f-4049-b686-1548b1ee5e14",
"status": "ACTIVE",
"friendly_name": "Private trust stack setup",
"api_token": {
"id": "cdfcf47d-b47f-4919-bd1b-c62935312cea",
"token": "<returned-once; redacted>",
"enabled": true
},
"applications": [
{
"id": "e70323fa-6014-42f3-a669-22645606d1fd",
"name": "CA Manager",
"permissions": ["VIEW_CM_CA", "MANAGE_CM_CA_ACCOUNTS"]
},
{
"id": "c46187a2-243b-41a9-aedc-0518ab1b6cf6",
"name": "Device Trust",
"roles": ["<selected-device-trust-role-name>"],
"permissions": ["<effective-device-trust-permission-codes>"]
},
{
"id": "a7081787-1dd8-492a-9c90-26ef079a7eb1",
"name": "Software Trust",
"permissions": [
"MANAGE_SM_CERTIFICATE_PROFILE",
"VIEW_SM_CERTIFICATE_TEMPLATE",
"VIEW_SM_KEYPAIR",
"GENERATE_SM_KEYPAIR",
"GENERATE_SM_CERTIFICATE",
"SIGN_SM_HASH"
]
}
]
}
Extract the identifiers and returned-once token without printing the token:
jq -er '.api_token.token' service-user-response.json > service-api-token.txt
jq -er '.id' service-user-response.json > service-user-id.txt
jq -er '.api_token.id' service-user-response.json > api-token-id.txt
chmod 600 service-api-token.txt service-user-id.txt api-token-id.txt service-user-response.json
SERVICE_USER_ID="$(<service-user-id.txt)"
API_TOKEN_ID="$(<api-token-id.txt)"
SERVICE_API_TOKEN="$(<service-api-token.txt)"
export SERVICE_USER_ID API_TOKEN_ID
Save the user id as service_user_id, api_token.id as api_token_id, and the returned token as service_api_token. You need both IDs to remove the credentials later and associate them with audit events.
Confirm the roles grant the required permissions
The role list does not report permissions. Compare the service user’s granted permissions against the required capabilities, and stop if any are missing. Every later phase depends on this result.
REQUIRED_PERMISSIONS_JSON='[
"VIEW_CM_CA",
"MANAGE_CM_CA_ACCOUNTS",
"MANAGE_SM_CERTIFICATE_PROFILE",
"VIEW_SM_CERTIFICATE_TEMPLATE",
"VIEW_SM_KEYPAIR",
"GENERATE_SM_KEYPAIR",
"GENERATE_SM_CERTIFICATE",
"SIGN_SM_HASH"
]'
jq -er --argjson required "${REQUIRED_PERMISSIONS_JSON}" '
[.applications[]?.permissions[]?] as $granted |
($required - $granted) as $missing |
if ($missing | length) == 0 then true
else error("the assigned roles do not grant: " + ($missing | join(", ")))
end' service-user-response.json > /dev/null
If the guard fails, return to Find the required role names, choose a different role for the product named in the error, and replace the service user rather than adding a broad administrator role.
The Device Trust Manager Solution Administrator value is a role, not a permission code, so it does not belong in REQUIRED_PERMISSIONS_JSON. Retrieve the user details to verify every assigned role and review all effective permissions, not only those required by the guard:
curl --fail-with-body --silent --show-error \
"https://demo.one.digicert.com/account/api/v1/user/${SERVICE_USER_ID}" \
-H "x-api-key: ${BOOTSTRAP_API_TOKEN}" \
-o service-user-details.json
Review all assigned roles and effective permissions:
jq '{
user_type,
status,
applications: [.applications[] | {name, roles, permissions}]
}' service-user-details.json
Confirm that the response contains every role name you selected:
jq -er --argjson required_roles "${ROLE_NAMES_JSON}" '
[.applications[]?.roles[]?] as $assigned |
($required_roles - $assigned) as $missing |
if ($missing | length) == 0 then true
else error("the service user is missing assigned roles: " + ($missing | join(", ")))
end' service-user-details.json > /dev/null
Confirm that the selected role under device_trust_manager is the active Solution Administrator role for your tenant. This role has edit access to the divisions, certificate profiles, certificate policies, and device groups that the workflow creates. Record the assigned role names, and compare all returned permission codes against the capability table and the product’s role definition. Permissions beyond the workflow’s needs indicate an over-privileged setup credential. Use that credential only for this recorded demo setup, not for the production identities.
Step 1.5: Create the service user’s client certificate
The client authentication certificate must belong to the same service user whose API token you use with Software Trust Manager. Generate the private key locally, submit only the CSR, and authenticate the request with the service-user API token returned when you create the setup service user. Send the token in the x-api-key header. The API associates the new certificate with the authenticated service user and never receives the private key.
Set CLIENT_AUTH_CERTIFICATE_EXPIRATION to an approved UTC timestamp no more than 397 days in the future. The Account Manager API requires the expiration_date property:
CLIENT_AUTH_CERTIFICATE_NAME="Private trust stack client authentication"
CLIENT_AUTH_CERTIFICATE_EXPIRATION="2027-01-31T23:59:59Z"
Generate the private key and certificate signing request (CSR) locally. Only the CSR is sent to DigiCert:
openssl req -new -newkey rsa:2048 -nodes \
-keyout client.key \
-out client.csr \
-subj "/CN=Private trust stack setup"
chmod 600 client.key client.csr
Submit the CSR as the service user:
jq -n \
--rawfile csr client.csr \
--arg name "${CLIENT_AUTH_CERTIFICATE_NAME}" \
--arg expiration_date "${CLIENT_AUTH_CERTIFICATE_EXPIRATION}" '
{
csr: $csr,
name: $name,
expiration_date: $expiration_date
}' |
curl --fail-with-body --silent --show-error \
-X POST "https://demo.one.digicert.com/account/api/v1/client-auth-certificate" \
-H "x-api-key: ${SERVICE_API_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @- \
-o client-auth-certificate-response.json
Extract the certificate, CA certificate, and credential ID from the response:
jq -er '.x509_cert' client-auth-certificate-response.json > client.crt
jq -er '.ca_cert' client-auth-certificate-response.json > client-auth-ca.pem
jq -er '.id' client-auth-certificate-response.json > client-auth-certificate-id.txt
CLIENT_AUTH_CERTIFICATE_ID="$(<client-auth-certificate-id.txt)"
export CLIENT_AUTH_CERTIFICATE_ID
chmod 600 \
client.crt \
client.key \
client-auth-ca.pem \
client-auth-certificate-id.txt \
client-auth-certificate-response.json
Determine the actual expiration from the response end_date and the certificate’s notAfter value. The API might adjust the requested expiration boundary instead of returning the exact expiration_date value. Before using the certificate, confirm that its actual validity period complies with your credential policy.
Confirm that the certificate and private key match, that the certificate validates against the returned CA certificate, and that the actual expiration complies with policy:
test "$(openssl x509 -in client.crt -pubkey -noout | openssl sha256)" = \
"$(openssl pkey -in client.key -pubout | openssl sha256)"
openssl verify -CAfile client-auth-ca.pem client.crt
jq '{end_date}' client-auth-certificate-response.json
openssl x509 -in client.crt -noout -enddate
Verify that mutual TLS (mTLS) authenticates the intended service user:
curl --fail-with-body --silent --show-error \
--cert client.crt \
--key client.key \
"https://clientauth.demo.one.digicert.com/account/api/v1/user/me" |
jq -e --arg user_id "${SERVICE_USER_ID}" '
if .id == $user_id then true
else error("client certificate belongs to an unexpected user")
end' > /dev/null
Phase 1 checkpoint
-
account_ididentifies the intended active account. -
organization_ididentifies the approved active organization. -
GET /account/api/v1/user/{service_user_id}reports every selected role name; the CA and Software Trust permission checks pass; and the Device Trust role is the tenant’s active Solution Administrator role. -
service_api_tokenis stored in a secret manager or protected local file and does not appear in logs. -
client.crtandclient.keybelong to the service user and match cryptographically. - The response
end_datematches the certificate’snotAftervalue and complies with the approved validity policy.