What We’re Testing
After issuing X.509 certificates via the QuickZTNA CA, you need to download and install them on machines to enable TLS. This chapter covers:
- Downloading the certificate and private key from the API response (they are returned only once at issuance)
- Using the
ztna certCLI command which automates the download and file-writing - Installing the CA certificate into the system trust store so clients recognize QuickZTNA-issued certificates
- Verifying a TLS connection using the issued certificate and key
Key facts from the source code:
- The private key is never stored on the server. It is returned once in the
issueresponse and cannot be retrieved again. If lost, you must issue a new certificate. - The
ztna certCLI command writes files to disk automatically:{domain}.crtfor the certificate and{domain}.keyfor the private key (customizable via--cert-fileand--key-fileflags). - The CLI’s
--serve-demoflag starts a temporary HTTPS server on port 443 using the issued certificate, useful for quick verification. - Certificates use ECDSA P-256 (secp256r1), so the private key is in PKCS#8 format (
-----BEGIN PRIVATE KEY-----).
Your Test Setup
| Machine | Role |
|---|---|
| ⊞ Win-A | CLI certificate download, PowerShell for curl/OpenSSL verification |
| 🐧 Linux-C | Install CA trust, run test HTTPS server, verify TLS connections |
Prerequisites: CA initialized and functional (Chapter 56 ST1). Both machines authenticated and registered with ztna up.
ST1 — Download Certificate via CLI
What it verifies: The ztna cert command issues a certificate and writes the cert and key files to disk with correct permissions and content.
Steps:
- On ⊞ Win-A , ensure you are authenticated:
ztna status
- Request a certificate for a test domain:
ztna cert test-app.myorg.ztna
Expected output:
Requesting certificate for test-app.myorg.ztna...
Certificate written to test-app.myorg.ztna.crt
Private key written to test-app.myorg.ztna.key
Expires: 2026-04-16T...
- Verify the files exist and contain PEM data:
Get-Content test-app.myorg.ztna.crt | Select-Object -First 1
# Expected: -----BEGIN CERTIFICATE-----
Get-Content test-app.myorg.ztna.key | Select-Object -First 1
# Expected: -----BEGIN PRIVATE KEY-----
- Test with custom output paths:
mkdir C:\certs -Force
ztna cert test-app.myorg.ztna --cert-file=C:\certs\app.crt --key-file=C:\certs\app.key
Expected: Files written to C:\certs\app.crt and C:\certs\app.key.
- Test without a domain argument (uses machine name):
ztna cert
Expected: Uses the registered machine name as the domain. Output shows Requesting certificate for {machine-name}....
Pass: CLI writes both files. Certificate file contains PEM certificate. Key file contains PEM private key. Custom paths work.
Fail / Common issues:
- “not authenticated. Run ‘ztna login’ first” — CLI session has expired. Re-authenticate with
ztna login. - “not registered. Run ‘ztna up’ first” — the machine has no registration state. Run
ztna up. - “no domain specified and machine name is empty” — the machine’s name was not set during registration and no argument was provided. Specify the domain explicitly:
ztna cert my-domain.ztna.
ST2 — Download Certificate via API and Save Manually
What it verifies: Certificates issued via the API can be manually saved to files for use on any machine.
Steps:
- On ⊞ Win-A , issue a certificate via API:
$response = curl -s -X POST "https://login.quickztna.com/api/issue-certificate" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"action": "issue",
"org_id": "YOUR_ORG_ID",
"subject_cn": "api-download-test.internal"
}'
$response | python -m json.tool
- Extract and save the certificate:
$response | python -c "import sys,json; print(json.load(sys.stdin)['data']['certificate'])" > api-cert.pem
- Extract and save the private key:
$response | python -c "import sys,json; print(json.load(sys.stdin)['data']['private_key'])" > api-key.pem
- Extract and save the CA certificate (needed for trust):
curl -s -X POST "https://login.quickztna.com/api/issue-certificate" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{"action": "initialize_ca", "org_id": "YOUR_ORG_ID"}' `
| python -c "import sys,json; print(json.load(sys.stdin)['data']['ca_certificate'])" > ca.pem
Warning: Calling initialize_ca again rotates the CA key pair. If you already have a CA, retrieve its certificate from a previous response or from the certificate list. Only call initialize_ca if you intend to reset the CA.
- Verify all three files:
Get-Content api-cert.pem | Select-Object -First 1
# -----BEGIN CERTIFICATE-----
Get-Content api-key.pem | Select-Object -First 1
# -----BEGIN PRIVATE KEY-----
Get-Content ca.pem | Select-Object -First 1
# -----BEGIN CERTIFICATE-----
Pass: All three PEM files are saved correctly and contain valid PEM headers.
Fail / Common issues:
- JSON parsing errors — the
python -ccommand failed. Ensure Python is in your PATH and the API response was valid JSON. - Private key is
null— this should never happen for a successful issuance. Check thatsuccessistruein the response.
ST3 — Install CA Certificate on Linux
What it verifies: After adding the QuickZTNA CA certificate to the system trust store on Linux, TLS clients (curl, wget) recognize certificates issued by the CA.
Steps:
- On 🐧 Linux-C , copy the CA certificate to the system trust store:
For Ubuntu/Debian:
sudo cp ca.pem /usr/local/share/ca-certificates/quickztna-ca.crt
sudo update-ca-certificates
Expected output:
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.
For RHEL/CentOS/Fedora:
sudo cp ca.pem /etc/pki/ca-trust/source/anchors/quickztna-ca.crt
sudo update-ca-trust
- Verify the CA is now trusted:
openssl verify -CApath /etc/ssl/certs api-cert.pem
Or check that the system trust store includes the QuickZTNA CA:
awk -v cmd='openssl x509 -noout -subject' '/BEGIN/{close(cmd)};{print | cmd}' < /etc/ssl/certs/ca-certificates.crt | grep QuickZTNA
Expected: A line containing subject=O = QuickZTNA, CN = QuickZTNA CA.
- To remove the CA from the trust store later:
# Ubuntu/Debian
sudo rm /usr/local/share/ca-certificates/quickztna-ca.crt
sudo update-ca-certificates --fresh
# RHEL/CentOS/Fedora
sudo rm /etc/pki/ca-trust/source/anchors/quickztna-ca.crt
sudo update-ca-trust
Pass: update-ca-certificates reports 1 certificate added. The CA subject appears in the system trust bundle.
Fail / Common issues:
- “0 added” — the file extension must be
.crt(not.pem) for Debian-based systems. Rename if needed. - Permission denied — use
sudofor trust store operations.
ST4 — Verify TLS Connection with Issued Certificate
What it verifies: A TLS server using the QuickZTNA-issued certificate and key can serve HTTPS traffic, and clients with the CA trust can connect without certificate warnings.
Steps:
- On 🐧 Linux-C , start a test HTTPS server using OpenSSL:
# Using OpenSSL s_server (quick test)
openssl s_server -cert api-cert.pem -key api-key.pem -accept 8443 -www &
SERVER_PID=$!
- Test the connection from the same machine:
# With explicit CA file (should succeed)
curl -s --cacert ca.pem https://localhost:8443/ -o /dev/null -w "%{http_code}\n"
Expected: HTTP status 200.
- Test without the CA file (depends on whether the CA was installed system-wide in ST3):
# If CA is in system trust store
curl -s https://localhost:8443/ -o /dev/null -w "%{http_code}\n"
Expected: 200 if CA was installed in ST3. Otherwise, curl returns an error about an untrusted certificate.
- Test with OpenSSL s_client for detailed TLS info:
echo | openssl s_client -connect localhost:8443 -CAfile ca.pem 2>/dev/null | grep "Verify return code"
Expected:
Verify return code: 0 (ok)
- Stop the test server:
kill $SERVER_PID 2>/dev/null
- Alternatively, use the CLI’s built-in demo server on ⊞ Win-A :
# This starts a demo HTTPS server on port 443
ztna cert demo-server.myorg.ztna --serve-demo
In another terminal, test the connection:
curl -k https://demo-server.myorg.ztna/
Expected output:
Hello from demo-server.myorg.ztna! TLS is working.
Note: -k skips certificate verification. To verify properly, you would need the CA certificate in the trust store or pass --cacert ca.pem.
Pass: TLS connections succeed with the CA trust. openssl s_client reports Verify return code: 0 (ok). The CLI --serve-demo flag serves HTTPS traffic.
Fail / Common issues:
- “certificate verify failed” — the CA certificate was not provided or not installed. Use
--cacert ca.pemwith curl. - “unable to load certificate” — the PEM files are corrupted. Re-download from the API.
- Port 443 requires root/admin — use a higher port like 8443 for testing, or run with elevated privileges.
ST5 — Verify Private Key Non-Retrieval
What it verifies: The private key cannot be retrieved from the server after initial issuance. The list action returns certificate metadata but not the private key.
Steps:
- On ⊞ Win-A , list all certificates:
curl -s -X POST "https://login.quickztna.com/api/issue-certificate" `
-H "Authorization: Bearer YOUR_TOKEN" `
-H "Content-Type: application/json" `
-d '{
"action": "list",
"org_id": "YOUR_ORG_ID"
}' | python -m json.tool
-
Examine the response for any certificate entry. The fields returned are:
id,org_id,machine_id,serial_number,subject_cnfingerprint,certificate_pem,issued_at,expires_atrevoked,revoked_at,revoke_reason
-
Confirm that
private_keyis not present in any list entry. Thecertificate_pemis included (the public certificate), but the private key is never stored in the database. -
Also verify via the CRUD endpoint:
curl -s "https://login.quickztna.com/api/db/issued_certificates?org_id=YOUR_ORG_ID" `
-H "Authorization: Bearer YOUR_TOKEN" | python -m json.tool
The issued_certificates table schema does not include a private_key column. The columns are:
id, org_id, machine_id, serial_number, subject_cn, fingerprint, certificate_pem, issued_at, expires_at, revoked, revoked_at, revoke_reason.
- This means: if you lose the private key file, the only option is to issue a new certificate and revoke the old one.
Pass: The list action and CRUD endpoint do not return any private key data. The issued_certificates table has no private key column.
Fail / Common issues:
- Private key appears in the list response — this would be a critical security vulnerability. Private keys should only be returned once at issuance time.
- The
certificate_pemcontains the private key — verify that the stored PEM starts with-----BEGIN CERTIFICATE-----, not-----BEGIN PRIVATE KEY-----.
Summary
| Sub-test | What it proves |
|---|---|
| ST1 | ztna cert CLI downloads and writes certificate + key files with correct PEM content |
| ST2 | Certificates can be manually extracted from the API response and saved to files |
| ST3 | CA certificate can be installed into the Linux system trust store |
| ST4 | TLS connections succeed using issued certificates; openssl s_client verifies the chain |
| ST5 | Private keys are never stored on the server and cannot be retrieved after issuance |