Skip to content

Certificate Management

When you visit a website over HTTPS, your browser checks that the website’s certificate was signed by a trusted authority — like a digital passport stamp that proves the site is who it claims to be. For Ghost to intercept and read HTTPS traffic (so you can inspect it), Ghost needs to act as that trusted authority. It does this by running its own Certificate Authority (CA) — essentially a private “passport office” that issues certificates on-the-fly for every website you visit. Your device trusts Ghost’s CA, Ghost creates a certificate that says “I am example.com,” your browser accepts it, and Ghost can decrypt the traffic to show you what’s happening.

This page covers the entire certificate lifecycle: how Ghost creates its CA, how it generates per-host certificates in real time, how it caches them for performance, how it installs the CA into your operating system’s trust store, and how it delivers certificates to mobile devices.

The Certificate Authority is Ghost’s root of trust. Every certificate Ghost generates is signed by this CA. If a device trusts the CA, it trusts all certificates the CA signs — which means Ghost can create a valid-looking certificate for any website.

When Ghost starts for the first time, it creates a root CA and saves it to disk. On every subsequent start, it loads the existing CA from disk instead of creating a new one — so you only need to trust the certificate once.

PropertyValueWhy
AlgorithmECDSA P-256Modern elliptic curve cryptography — fast to sign, small keys, widely supported. P-256 (also called secp256r1) is the most common curve, accepted by all browsers and operating systems.
Serial Number128-bit cryptographically randomRFC 5280 requires every certificate to have a unique serial number (up to 20 bytes). Ghost generates a random 128-bit integer using the operating system’s secure random number generator (crypto/rand).
SubjectO=Ghost Proxy, CN=Ghost Root CAThe “name” on the certificate. O (Organization) identifies Ghost, CN (Common Name) identifies this specific certificate. You’ll see “Ghost Root CA” in your keychain or certificate manager.
Validity10 years, backdated 1 hourThe certificate is valid from “1 hour ago” until “10 years from now.” The 1-hour backdating handles clock skew — if a device’s clock is slightly behind, it won’t reject the certificate as “not yet valid.”
Key UsageCertSign + CRLSignThis certificate can only sign other certificates and Certificate Revocation Lists. It cannot be used as a server certificate itself — it’s purely an authority.
Basic ConstraintsCA=true, MaxPathLen=0CA=true declares this is a Certificate Authority. MaxPathLen=0 means it cannot create intermediate CAs — it can only sign leaf (end-entity) certificates directly. This limits the blast radius if the CA key is compromised.

The CA is self-signed — it signs itself, which is normal for root CAs. (Root CAs are trusted because you explicitly install them, not because someone else vouched for them.)

The CA is stored at ~/.ghost/ (which is $HOME/.ghost/ on macOS/Linux, %USERPROFILE%\.ghost\ on Windows):

FilePermissionsContents
ca.crt0644 (world-readable)The public certificate in PEM format. This is what gets distributed to devices — it contains no secrets.
ca.key0600 (owner-only)The private key in PEM format (block type EC PRIVATE KEY). This is the secret that lets Ghost sign certificates. If someone else gets this file, they could create certificates that your devices would trust.

If writing the certificate file fails after the key has already been written, Ghost cleans up by deleting the key file — preventing a state where a key exists without its matching certificate.

On subsequent starts, LoadOrCreateCA() validates the existing CA before using it:

  1. PEM decode — reads both files and decodes the PEM blocks (the -----BEGIN CERTIFICATE----- wrapper format)
  2. Parse certificate — converts the binary certificate data (DER format) into a structured object
  3. Verify CA flag — checks that IsCA is true. If someone replaced the CA file with a regular certificate, this catches it.
  4. Check expiry — verifies the current time falls between NotBefore and NotAfter. An expired CA would produce leaf certificates that browsers reject.
  5. Parse private key — decodes and validates the ECDSA private key
  6. Key-certificate match — verifies the private key’s public component matches the certificate’s public key. This catches situations where files from different CAs got mixed up.

If validation fails because the files don’t exist, Ghost generates a new CA. If the files exist but are invalid (corrupted, expired, mismatched), Ghost returns an error rather than silently regenerating — because regenerating would invalidate all previously trusted installations, requiring users to re-trust the new CA on every device.

When your browser connects to example.com through Ghost, Ghost needs a certificate that says “I am example.com.” These are called leaf certificates (or end-entity certificates) — they’re the “leaves” at the end of the certificate chain, as opposed to the CA “root.”

Ghost generates these certificates on-the-fly as new hosts are encountered, caches them for reuse, and uses a single-flight mechanism to avoid duplicate work.

What this diagram shows — the certificate lookup flow:

  1. A TLS handshake begins — the browser is trying to establish a secure connection to a host through Ghost’s proxy
  2. Ghost’s CertIssuer first checks its LRU cache (a bounded dictionary that evicts the least-recently-used entry when full). If a valid, unexpired certificate exists for this host, it’s returned immediately — no generation needed
  3. If the cache doesn’t have it (cache miss), Ghost checks if another connection is already generating a certificate for the same host (the “inflight map”). If yes, it waits for that generation to finish and uses the same result — this prevents 50 simultaneous connections to api.example.com from generating 50 identical certificates
  4. If nobody is generating one yet, Ghost creates a new leaf certificate, signs it with the CA, attaches an OCSP response, stores it in the cache, and returns it
  5. The entire process happens during the TLS handshake — the browser is waiting, so speed matters

Before any lookup, the host is normalized:

  • Port is stripped (example.com:443 becomes example.com)
  • Converted to lowercase (Example.COM becomes example.com)
  • Whitespace is trimmed

This ensures Example.COM:443 and example.com both resolve to the same cached certificate.

PropertyValueWhy
AlgorithmECDSA P-256 (new key per host)Each leaf certificate gets its own unique private key. Even if one key is compromised, others remain safe.
Serial Number128-bit cryptographically randomSame generation method as the CA serial — unique per certificate.
SubjectCN=<host>, O=Ghost ProxyThe certificate identifies itself as the target host. Browsers check this matches the URL they’re connecting to.
Validity24 hours, backdated 1 hourShort-lived by design. If a leaf certificate somehow leaks, it expires quickly. The 1-hour backdate handles clock skew.
Key UsageDigitalSignature onlyThis certificate can only prove identity — it cannot sign other certificates.
Extended Key UsageServerAuth onlyRestricted to TLS server authentication — can’t be used for email signing, code signing, or other purposes.

SANs are the modern way browsers verify certificate identity. Ghost sets SANs based on whether the host is a domain name or an IP address:

IP addresses (e.g., 192.168.1.1):

  • Sets IPAddresses to the parsed IP — browsers match this against the connection IP

Domain names (e.g., api.example.com):

  • Sets DNSNames to include the exact host: ["api.example.com"]
  • Wildcard logic: if the host is a subdomain (has at least two dots), Ghost also adds a wildcard SAN for the parent domain. The logic splits on the first dot — api.example.com splits into api and example.com. If the remainder (example.com) itself contains a dot, a wildcard is added: *.example.com. This means the same certificate works for api.example.com, cdn.example.com, or any other subdomain.
  • Top-level domains don’t get wildcards: example.com splits into example and com. Since com doesn’t contain a dot, no wildcard is added. This prevents creating *.com certificates (which would be both useless and dangerous).

Examples:

HostDNSNames in Certificate
api.example.comapi.example.com, *.example.com
cdn.shop.example.comcdn.shop.example.com, *.shop.example.com
example.comexample.com (no wildcard)
localhostlocalhost (no wildcard)
192.168.1.1(IP SAN, not DNS)

OCSP (Online Certificate Status Protocol) is a mechanism for checking whether a certificate has been revoked (declared invalid before its expiry). Normally, a browser would contact the CA’s OCSP server to ask “is this certificate still valid?” Ghost doesn’t run an OCSP server, so instead it staples (attaches) a pre-built OCSP response directly to every leaf certificate.

The stapled OCSP response says:

FieldValueMeaning
StatusGood”This certificate is valid and has not been revoked”
SerialNumberThe leaf cert’s serialIdentifies which certificate this response is about
ThisUpdatenow - 1 hour”This response has been valid since 1 hour ago” (clock skew protection)
NextUpdatenow + 24 hours”Check again after 24 hours” (matches the leaf cert’s 24-hour validity)
Signed byGhost’s CAThe CA acts as its own OCSP responder — both the issuer certificate and responder certificate are the CA

Why this matters: Without OCSP stapling, Windows (specifically its TLS library called “schannel”) reports “revocation status unknown” errors. Some browsers and applications block navigation entirely when they can’t verify a certificate’s revocation status. The stapled “good” response prevents all of these issues — the browser gets its answer without needing to contact an external server.

The returned TLS certificate includes a chain of two certificates:

  1. Leaf certificate — the just-generated certificate for the specific host
  2. CA certificate — Ghost’s root CA

This chain lets the browser verify: “this leaf cert was signed by Ghost’s CA, and I trust Ghost’s CA (because the user installed it).”

Ghost uses an LRU cache (Least Recently Used) from the hashicorp/golang-lru/v2 library to store generated certificates:

SettingDefaultDescription
Cache capacity10,000 certificatesConfigurable via cert_cache_size in ~/.ghost/config.toml. The LRU eviction policy means the least-recently-accessed certificate gets removed when the cache is full.
Expiry checkEvery accessBefore returning a cached certificate, Ghost checks time.Now().Before(expiresAt). Expired certs are removed from the cache and regenerated.
Cache statsCacheStats() methodReturns current cache size and capacity — exposed through the API for monitoring.

With 10,000 capacity, Ghost can cache certificates for 10,000 different hosts simultaneously. For typical web browsing and testing, this is far more than enough — you’d need to visit 10,000 unique domains in a 24-hour window to cause evictions.

When a popular website sends multiple parallel requests through Ghost (which is normal — browsers open 6+ connections per domain), they all need a certificate for the same host. Without deduplication, Ghost would generate the same certificate multiple times simultaneously — wasting CPU and time.

The single-flight mechanism uses a certFuture struct:

certFuture {
done chan struct{} // Closed when generation finishes
cert *tls.Certificate
err error
}

How it works:

  1. First goroutine to request api.example.com acquires a mutex lock, checks the cache (double-check under lock), creates a certFuture, stores it in the inflight map, and releases the lock
  2. It then generates the certificate outside the lock — generation involves cryptographic operations that take time, and holding the lock would block all other certificate requests
  3. Second, third, fourth goroutines requesting api.example.com acquire the lock, see the existing certFuture in the inflight map, release the lock, and wait on the done channel — they block until generation finishes
  4. When the first goroutine finishes, it sets future.cert (or future.err if something went wrong), closes the done channel (which unblocks all waiting goroutines), re-acquires the lock, removes the inflight entry, and adds the certificate to the LRU cache
  5. All waiting goroutines receive the same certificate

This means even with 100 simultaneous connections to the same host, only one certificate is generated.

For your browser to accept Ghost’s certificates, your operating system must trust Ghost’s CA. Ghost handles this differently on each platform.

Command:

Terminal window
security add-trusted-cert -p ssl -r trustRoot \
-k ~/Library/Keychains/login.keychain-db ~/.ghost/ca.crt
DetailValue
KeychainLogin Keychain (~/Library/Keychains/login.keychain-db) — user-level, not system-level
Admin requiredNo — but macOS will prompt for your keychain password
Trust scopeSSL only (-p ssl) — the CA is trusted for HTTPS but not for email signing, code signing, etc.
Trust levelRoot (-r trustRoot) — trusted as a root CA
Install timeout60 seconds — the password dialog may take time

Verification (two-step):

  1. Certificate presence: security find-certificate -c "Ghost Root CA" -a <keychain> (10-second timeout)
    • If output contains "could not be found" or "SecKeychainSearchCopyNext" — not installed
    • If output does not contain "Ghost Root CA" — not installed
  2. Trust settings: security dump-trust-settings (10-second timeout)
    • Looks for "Ghost Root CA" in trust settings
    • If found but "Number of trust settings : 0" — installed but not trusted (user needs to manually enable trust)
    • If dump-trust-settings command fails entirely — assumes installed (conservative fallback)

Both checks must pass for Ghost to report the certificate as installed and trusted.

Command:

Terminal window
certutil -addstore -user Root ca.crt
DetailValue
StoreUser Root Store (-user Root) — per-user, not machine-wide
Admin requiredNo — the -user flag installs to the current user’s store, no UAC elevation prompt
Timeout10 seconds

Verification: certutil -verifystore -user Root (10-second timeout). Ghost scans the output for "Ghost Root CA". Non-zero exit codes are tolerated because verifystore may return errors for unrelated certificates in the store.

Linux certificate trust varies by distribution. Ghost auto-detects the distribution by reading /etc/os-release and checking the ID and ID_LIKE fields:

Distro FamilyDetected IDsCertificate PathUpdate CommandRequires
Debian/Ubuntudebian, ubuntu, linuxmint, pop (also derivatives via ID_LIKE)/usr/local/share/ca-certificates/ghost-ca.crtupdate-ca-certificatessudo
Fedora/RHELfedora, rhel, centos, rocky, alma/etc/pki/ca-trust/source/anchors/ghost-ca.crtupdate-ca-trustsudo
Archarch, manjaro, endeavouros/etc/ca-certificates/trust-source/anchors/ghost-ca.crttrust anchor --store (preferred), fallback to trust extract-compat or update-ca-trustsudo
SUSEopensuse, sles/etc/pki/trust/anchors/ghost-ca.crtupdate-ca-certificatessudo
UnknownAnything elseTries Debian path first, then Fedora path. If both fail, returns an error with manual instructions.sudo

All Linux commands have a 30-second timeout. The certificate file is copied with 0644 permissions, and parent directories are created with 0755 if they don’t exist.

Verification: Ghost checks if the certificate file exists at the expected path for the detected distribution. For unknown distributions, it checks all four paths.

On platforms other than macOS, Windows, and Linux (e.g., FreeBSD), all certificate operations return ErrUnsupported. Users must manually install the CA certificate.

Mobile devices can’t install certificates through command-line tools — they need to download and install them through the browser or device settings. Ghost provides several delivery mechanisms, all served through the proxy port itself (no API authentication required, since the device may not have Ghost’s API credentials).

URLContent-TypeContent-DispositionPurpose
http://<proxy-address>/ghost-ca.crtapplication/x-x509-ca-certattachment; filename="ghost-ca.crt"Direct CA certificate download. Works on any host — the path /ghost-ca.crt is intercepted regardless of the Host header.
http://<proxy-address>/ghost-profile.mobileconfigapplication/x-apple-aspen-configattachment; filename="ghost-proxy.mobileconfig"iOS Configuration Profile that installs and trusts the CA in one step.
http://ghost.proxy/text/htmlBranded landing page with download links and setup instructions.
http://ghost.proxy/verifyapplication/jsonReturns {"connected": true, "client_ip": "<ip>"} with CORS headers — used to verify a device can reach Ghost.

All download endpoints set Cache-Control: no-store to prevent caching (since the CA could be regenerated).

When a device navigates to http://ghost.proxy/, Ghost serves a branded HTML page with:

  • Ghost branding — gradient-styled header with “Traffic Intelligence Proxy” subtitle
  • Download button — links to /ghost-ca.crt for direct CA download
  • Info panel — shows the proxy address, certificate URL, host IP, and port
  • iOS Quick Setup — 4-step guide: (1) download certificate, (2) allow profile download, (3) install profile in Settings, (4) enable full trust in Certificate Trust Settings
  • Android Quick Setup — 3-step guide: (1) download certificate, (2) open Security settings, (3) select the downloaded file

This page works without DNS because Ghost intercepts requests where the Host header is ghost.proxy (or ghost.proxy:<port>) directly in the proxy — no DNS resolution is needed.

The landing page also reverse-proxies two endpoints for injected scripts: /api/v1/script/fetch and /api/v1/script/analyze are forwarded to Ghost’s API server. This avoids mixed-content issues when scripts injected into HTTPS pages need to communicate with Ghost.

The .mobileconfig file is an Apple Configuration Profile — an XML document that iOS understands natively. When a user opens this file on an iOS device, iOS presents a system dialog to install the profile.

Profile structure:

FieldValue
PayloadTypeConfiguration (top-level container)
PayloadIdentifiercom.ghost.proxy.profile
PayloadOrganizationGhost
PayloadRemovalDisallowedfalse — users can remove the profile at any time
PayloadVersion1
PayloadUUIDRandom v4 UUID (generated fresh each time)

The profile contains a single payload:

FieldValue
PayloadTypecom.apple.security.root — installs a root certificate
PayloadIdentifiercom.ghost.proxy.cert
PayloadCertificateFileNameghost-ca.crt
PayloadContentBase64-encoded DER of the CA certificate
PayloadDisplayNameGhost CA Certificate
PayloadDescription”Installs the Ghost proxy CA certificate for HTTPS inspection.”
PayloadUUIDRandom v4 UUID (different from the top-level UUID)

Consent text (shown to the user before installation):

“This profile installs the Ghost CA certificate for HTTPS inspection. You will also need to configure the HTTP proxy in your Wi-Fi settings. Remove this profile when you no longer need traffic capture.”

The profile does not include proxy settings (com.apple.proxy.http.global). This would require a supervised (MDM-managed) device, which most devices aren’t. Users configure the proxy manually in Wi-Fi settings.

Two random v4 UUIDs are generated for each download — one for the profile container and one for the certificate payload. These are different every time, which means downloading the profile twice creates two distinct profiles (though iOS will recognize they have the same identifier and offer to replace the existing one).

The .mobileconfig endpoint is also available through the API at GET /api/v1/mobile/profile — this is the same template, served through the authenticated API for Ghost’s UI to use.

For iOS Simulators (Xcode’s virtual iPhones/iPads), Ghost can install the CA certificate directly without user interaction:

Command:

Terminal window
xcrun simctl keychain <UDID> add-root-cert <temp-file>
DetailValue
EndpointPOST /api/v1/mobile/simulators/install-cert
Request body{"udid": "<UDID>"} (1024-byte body limit)
UDID validationRegex: ^[A-Za-z0-9][A-Za-z0-9.:_-]*$ — prevents command injection
ProcessWrites CA PEM to a temp file → runs xcrun simctl keychain → deletes temp file
Timeout10 seconds
PlatformmacOS only — returns 400 on other platforms

The simulator trusts the certificate immediately — no user interaction needed. This is possible because simulators run in a sandboxed environment where simctl has full control.

For Android Emulators, Ghost pushes the certificate file to the emulator’s storage:

Command:

Terminal window
adb -s <serial> push <temp-file> /sdcard/ghost-ca.crt
DetailValue
EndpointPOST /api/v1/mobile/emulators/push-cert
Request body{"serial": "<serial>"} (1024-byte body limit)
Serial validationSame regex as UDID — ^[A-Za-z0-9][A-Za-z0-9.:_-]*$
ProcessWrites CA PEM to a temp file → runs adb push → deletes temp file
Timeout10 seconds
Manual step requiredUser must navigate to Android Settings → Security → Install from storage and select the file

Unlike iOS simulators, Android emulators can’t auto-trust certificates through ADB. The file is placed on the virtual SD card, but the user must complete installation through the Settings UI.

MethodEndpointHandlerDescription
GET/api/v1/cert/ca.crtdownloadCACertDownloads the CA certificate as a PEM file. Sets Content-Disposition: attachment; filename="ghost-ca.crt". Returns 500 if CA is not loaded.
GET/api/v1/cert/statusgetCertStatusReturns CA certificate metadata: loaded status, subject, issuer, validity dates, serial number, isCA flag. Returns {loaded: false} (HTTP 200) if CA isn’t loaded.
POST/api/v1/cert/installinstallCertInstalls the CA to the OS trust store. Returns 501 if the platform is unsupported, 500 on failure, 200 {ok: true} on success.
GET/api/v1/cert/installedgetCertInstalledChecks if the CA is installed in the OS trust store. Returns {installed: bool, store: string, method: string}.
POST/api/v1/mobile/simulators/install-certinstallSimulatorCertInstalls CA to an iOS Simulator’s keychain via xcrun simctl. macOS only.
POST/api/v1/mobile/emulators/push-certpushEmulatorCertPushes CA to an Android emulator’s SD card via adb. User must finish installation in Settings.
GET/api/v1/mobile/profileDownloads the iOS .mobileconfig profile (same as proxy endpoint, but through authenticated API).

Certificate-related settings in ~/.ghost/config.toml:

[proxy]
# Maximum number of leaf certificates to keep in cache.
# Default: 10000. Higher values use more memory but reduce
# certificate generation for sites with many subdomains.
cert_cache_size = 10000
# Hosts that should NOT be intercepted (certificate pinning, etc).
# Ghost passes traffic through without decrypting.
# Supports exact matches and wildcard suffixes.
ssl_bypass_hosts = ["*.apple.com", "*.icloud.com"]

CA key protection — The ca.key file has 0600 permissions (owner read/write only). If an attacker obtains this file, they could generate certificates trusted by any device that has Ghost’s CA installed. Keep ~/.ghost/ secure.

24-hour leaf validity — Short-lived certificates limit the window of exposure if a leaf certificate is somehow compromised. After 24 hours, the certificate expires and a new one must be generated.

No intermediate CAsMaxPathLen=0 prevents the CA from signing other CAs. Even if the CA key is compromised, the attacker can only create leaf certificates, not new CAs.

UDID/serial validation — Both the simulator and emulator endpoints validate device identifiers with a strict regex before passing them to shell commands, preventing command injection attacks.

Body size limits — Mobile API endpoints cap request bodies at 1,024 bytes, preventing denial-of-service through oversized payloads.