Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Run an Internal Certificate Authority with step-ca

By Kokil Thapa | Last reviewed: September 2026

Public CAs work for customer-facing HTTPS, but staging clusters, VPN endpoints, and admin panels need a different model. When you Run an Internal Certificate Authority with step-ca, you get a small, auditable PKI that issues short-lived X.509 certificates on demand. PKI and certificate management basics still apply — root trust, intermediates, and revocation — but step-ca wraps them in a single binary with ACME, JWT, and SSH provisioners. This guide walks through a production-minded setup on Ubuntu 24.04 that I would ship on a real client project.

Why should you Run an Internal Certificate Authority with step-ca?

Commercial CAs charge per hostname and enforce rate limits. Internal hostnames like api.staging.internal or grafana.ops.local do not belong on a public trust chain anyway. A private CA gives you full control over validity periods, SAN lists, and revocation policy.

step-ca from Smallstep is a Go-based CA server that speaks ACME v2, so tools like Certbot and Caddy work without custom plugins. It also supports JWT and SSH certificate provisioners for mTLS and short-lived SSH access. Compared to OpenSSL scripts or cfssl, step-ca ships with a coherent CLI (step), sane defaults, and active maintenance.

Internal PKI with step-caRoot CAOffline keyIntermediate CAstep-ca server signs hereWeb TLSnginx / ApacheAPI mTLSservice meshSSH Certsbastion hostsClients trust root_ca.crt — not publicly trusted
Run an Internal Certificate Authority with step-ca: root offline, intermediate online, leaf certs for internal workloads

On legal-tech portals and client portals I have maintained, internal staging environments mirror production TLS behaviour. That catches certificate chain errors before they reach users. step-ca makes that repeatable without manual OpenSSL invocations each sprint.

ApproachBest forAutomationOperational cost
Public CA (Let's Encrypt)Public DNS namesACME, excellentFree, rate-limited
step-ca internal CAPrivate DNS, mTLS, SSHACME + JWT + SSHOne VM, low
Manual OpenSSL CAOne-off certsNoneHigh human error
Cloud KMS CA (AWS PCA)Enterprise AWS shopsAPI-drivenRs 15,000+/month (~USD 110)

For teams already running Ubuntu servers with Linux system administration workflows, step-ca fits naturally beside existing nginx and PHP-FPM stacks.

How do you install and initialize step-ca on Ubuntu?

Start on a dedicated VM — not your application server. The CA holds signing keys and should sit on a restricted network segment. Ubuntu 22.04 or 24.04 both work; I prefer 24.04 for current OpenSSL behaviour.

Install the step CLI and step-ca

Download the latest release from the official Smallstep GitHub repository. Pin the version in your deployment notes so upgrades are deliberate.

curl -LO https://dl.smallstep.com/gh-release/certificates/docs-ca-install/v0.27.2/step-ca_linux_amd64.tar.gz
tar -xf step-ca_linux_amd64.tar.gz
sudo install step-ca /usr/local/bin/step-ca
curl -LO https://dl.smallstep.com/gh-release/cli/docs-cli-install/v0.27.2/step_linux_amd64.tar.gz
tar -xf step_linux_amd64.tar.gz
sudo install step /usr/local/bin/step

Verify both binaries respond before continuing:

step-ca version
step version

Initialize the CA hierarchy

The step ca init command creates a root CA, an intermediate CA, default provisioners, and a ca.json configuration file. Use a strong password and store it in your secrets manager — not in shell history.

step ca init \
  --deployment-type standalone \
  --name "MyOrg Internal CA" \
  --dns ca.internal.example.com \
  --address :443 \
  --provisioner admin@example.com

mkdir -p /etc/step-ca
mv ca.json /etc/step-ca/
mv secrets/ /etc/step-ca/

This produces root_ca.crt, intermediate key material under secrets/, and a JWK provisioner for admin operations. Read root CA vs intermediate CA if the two-tier model is new to you — the intermediate is what step-ca uses daily while the root stays offline.

step-ca Setup WorkflowInstallstep + step-castep ca initroot + intermediateConfigureACME provisionerStart CAsystemd unitHarden before productionOffline root · chmod 600 keys · firewall :443Backup secrets/encrypted off-siteDistribute rootto dev laptops + CITest issuancebefore cutover
Initialize step-ca, enable ACME, harden key storage, then distribute trust before issuing production certificates

Run step-ca as a systemd service

Create a dedicated system user and point systemd at your config. Reload PHP-FPM is unrelated here — but the same discipline applies on servers where I run Deployer releases.

sudo useradd --system --home /etc/step-ca --shell /usr/sbin/nologin stepca
sudo chown -R stepca:stepca /etc/step-ca
sudo chmod 600 /etc/step-ca/secrets/*
# /etc/systemd/system/step-ca.service
[Unit]
Description=step-ca Internal Certificate Authority
After=network.target

[Service]
User=stepca
Group=stepca
Environment=STEPPATH=/etc/step-ca
ExecStart=/usr/local/bin/step-ca /etc/step-ca/ca.json \
  --password-file /etc/step-ca/secrets/password.txt
Restart=on-failure
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now step-ca
sudo systemctl status step-ca

Place the CA behind nginx or Caddy if you want TLS termination on port 443 with a public-facing hostname for the CA itself. The CA API can also listen on an internal port like 8443 if you prefer.

How do you configure ACME and issue certificates with step-ca?

ACME is the highest-value provisioner for most teams. It lets Certbot, acme.sh, Traefik, and Caddy request certificates without custom integration code. The ACME endpoint lives at https://ca.internal.example.com/acme/acme/directory by default.

Enable the ACME provisioner

Edit /etc/step-ca/ca.json and confirm an ACME-type provisioner exists. If you initialized with defaults, add one explicitly:

step ca provisioner add acme --type ACME

Restart step-ca after any config change. Then confirm the directory URL responds:

curl -k https://ca.internal.example.com/acme/acme/directory | jq .

The response should list newNonce, newOrder, and newAccount endpoints. Cross-check against the ACME RFC 8555 specification if you debug protocol errors.

ACME Issuance via step-caACME ClientCertbot / Caddystep-caACME directoryChallengeHTTP-01 checkDomain ownership provedSign CSRintermediate keyLeaf cert + chain deliveredTrust root_ca.crton every client
ACME clients request certificates from step-ca after completing HTTP-01 or TLS-ALPN-01 domain validation

Issue a certificate with Certbot

Install the root CA on the requesting machine first. Then point Certbot at your private ACME directory:

sudo cp root_ca.crt /usr/local/share/ca-certificates/myorg-internal.crt
sudo update-ca-certificates

sudo certbot certonly --standalone \
  --server https://ca.internal.example.com/acme/acme/directory \
  --domain staging-api.internal.example.com \
  --email ops@example.com \
  --agree-tos \
  --non-interactive

Certbot stores the cert under /etc/letsencrypt/live/ even though the issuer is your internal CA. Wire those paths into nginx the same way you would for a public cert. See install SSL certificates on Ubuntu for nginx virtual-host wiring.

Issue with the step CLI directly

For quick one-off certs or CI pipelines, the native CLI is faster than ACME:

step ca certificate staging.internal.example.com staging.crt staging.key \
  --ca-url https://ca.internal.example.com \
  --root root_ca.crt

Inspect the result with OpenSSL:

openssl x509 -in staging.crt -noout -subject -issuer -dates -ext subjectAltName

Understanding fields like subjectAltName and validity windows helps when debugging mTLS failures. Anatomy of an X.509 certificate covers each extension in detail.

How do you distribute trust and automate certificate renewal?

An internal CA is useless if clients reject the chain. Every laptop, CI runner, browser, and service that connects must trust your root — and only your root.

Distribute the root CA certificate

  1. Export root_ca.crt from the initialization output.
  2. Add it to OS trust stores: macOS Keychain, Windows certmgr, Linux update-ca-certificates.
  3. Configure language runtimes that maintain separate stores — Java cacerts, Node.js NODE_EXTRA_CA_CERTS, Python REQUESTS_CA_BUNDLE.
  4. Document the fingerprint so engineers can verify they imported the correct root.

For automated trust in CI, store the root as a base64-encoded secret and decode at job start. The base64 encoder and decoder on this site helps when preparing those secrets locally.

Automate renewal

Short-lived certificates — 24 hours to 90 days — limit exposure when a key leaks. step-ca defaults favour shorter lifetimes than public CAs. Schedule renewal before expiry:

# Cron on the application server — renew at 02:00 daily
0 2 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"

Pair this with monitoring that alerts when a cert expires in under seven days. I treat cert expiry like disk space — predictable until it is not. Read automate certificate rotation for patterns that apply beyond step-ca.

On Kubernetes, cert-manager supports custom ACME endpoints. Point its ClusterIssuer at your step-ca directory URL and let it handle ingress TLS automatically. That pattern scales better than shell scripts on every node.

Certificate Lifetime StrategyPublic CA (Let's Encrypt)90-day max validityAuto-renew at 30 daysBrowser trusted by defaultstep-ca Internal CA1 hour to 90 days (your policy)Renew via ACME or cronManual root trust requiredRecommended: 30-day internal certs with weekly renewal checkMonitor expiryPrometheus / cronRevoke if neededCRL or OCSPAudit logsstep-ca JSON logs
Internal step-ca certificates need shorter lifetimes, automated renewal, and explicit trust distribution unlike public CA certs

What security practices matter when operating step-ca in production?

A compromised CA key is worse than a compromised server key. An attacker can mint arbitrary trusted certificates across your entire infrastructure. Treat the CA VM like a crown jewel.

Protect root and intermediate keys

  • Keep the root CA private key offline after signing the intermediate CSR.
  • Store intermediate keys with chmod 600 and restrict ownership to the stepca user.
  • Use a hardware security module or cloud KMS for intermediate keys at scale.
  • Rotate the intermediate every one to two years; keep the root lifespan at 10+ years.

Generate strong passwords for encrypted key files. A password generator helps, but store the result in Vault or your team's secrets manager — never in Git.

Enable revocation and audit logging

step-ca supports CRL and OCSP for certificate revocation. Enable at least one method before production cutover. Certificate revocation: CRL vs OCSP explains the trade-offs.

# ca.json excerpt — enable CRL
"crl": {
  "enabled": true,
  "expiry": "24h",
  "cacheDuration": "6h"
}

Ship step-ca logs to your central logging stack. Every issuance event should record the requester, SAN list, and serial number. That audit trail matters for compliance reviews on client portals handling sensitive documents.

Network and access controls

Restrict CA API access to internal networks only. Use UFW or security groups to allow port 443 from application subnets and deny the public internet. Require mTLS for admin provisioner operations in larger teams.

Do not expose the CA admin JWK provisioner to untrusted networks. If an admin key leaks, an attacker can issue any certificate your CA trusts. Separate admin access from ACME issuance paths.

How does step-ca fit Laravel, API, and client portal deployments?

Most of my production work runs Laravel on Ubuntu with nginx terminating TLS. Public sites use Let's Encrypt. Internal staging and service-to-service calls use step-ca.

For a Laravel API behind nginx, terminate TLS at nginx with a step-ca cert. Laravel sees HTTP on localhost and trusts the X-Forwarded-Proto header. For mTLS between microservices, configure nginx ssl_verify_client and pass verified client cert details to PHP via headers.

On portals like Mijar Law Associates, document uploads and payment callbacks depend on valid HTTPS in every environment. Matching TLS behaviour in staging prevents surprises during release week. API development projects with webhook integrations benefit the same way — test callback URLs against real certificate chains locally.

If your application integrates with Nepal government digital signature workflows, internal PKI is separate from NIC-issued DSC tokens. Read Nepal digital signature certificate for web apps for that distinct trust model.

For ongoing CA maintenance — renewals, trust store updates, revocation drills — support and maintenance retainer agreements cover the operational side so your team focuses on features.

Key Takeaways

  • Run step-ca on a dedicated VM with root offline and intermediate online for daily signing.
  • Enable the ACME provisioner so Certbot, Caddy, and cert-manager integrate without custom code.
  • Distribute root_ca.crt to every client, CI runner, and runtime trust store before issuing certs.
  • Use short validity periods (30 days or less) with automated renewal cron jobs and expiry monitoring.
  • Enable CRL or OCSP, restrict network access, and audit every issuance event.
  • Keep public Let's Encrypt certs for customer-facing domains; use step-ca only for internal names.

People Also Ask

Is step-ca free for commercial use?

Yes. step-ca is open source under the Apache 2.0 licence. Smallstep also sells enterprise support and hosted options, but the core CA server and CLI are free to run on your own infrastructure without licence fees.

Can step-ca replace Let's Encrypt for public websites?

No. Browsers do not trust your private root CA. Public websites still need Let's Encrypt or another publicly trusted CA. step-ca is for internal hostnames, mTLS, VPN, and staging environments where you control client trust.

How do you back up a step-ca installation?

Back up the entire /etc/step-ca directory including secrets/, ca.json, and the root CA certificate. Store encrypted backups off-site. Test restoration quarterly — a backup you have never restored is a guess.

Does step-ca work with Docker and Kubernetes?

Yes. Run step-ca in a container with secrets mounted as volumes. On Kubernetes, cert-manager supports custom ACME directory URLs pointing at step-ca. Keep persistent storage for the CA database if you enable revocation tracking.

Deploy your internal CA with confidence

You now have a complete path to Run an Internal Certificate Authority with step-ca: install, initialize, enable ACME, distribute trust, and automate renewal. Start in staging, verify chain trust end to end, then expand to internal services. The official step-ca documentation covers advanced provisioners, RA mode, and HSM integration when you outgrow a single VM.

If you want help hardening PKI for a Laravel portal, API platform, or multi-environment deployment, contact us to plan the rollout. You can also review how we handle production infrastructure on the Notary Nepal portal and explore related reading on SSL/TLS certificates explained and the certificate chain of trust.

Frequently Asked Questions

step-ca is a Go-based CA server from Smallstep that issues short-lived X.509 certificates on demand. It speaks ACME v2 plus JWT and SSH provisioners, so Certbot, Caddy, Traefik, and cert-manager integrate without custom plugins. Compared to manual OpenSSL scripts or cfssl, it ships with the step CLI, sane defaults, and active maintenance. Use it when internal hostnames like api.staging.internal need TLS but do not belong on a public trust chain.

Yes. step-ca is open source under the Apache 2.0 licence with no licence fees on your own infrastructure.

No. Browsers do not trust your private root CA. Public sites still need Let's Encrypt or another publicly trusted CA.

Run step-ca on a dedicated VM, not an application server. Download step-ca and the step CLI from the official Smallstep GitHub release (v0.27.2 in this guide), install both to /usr/local/bin, and verify with step-ca version and step version. Run step ca init with --deployment-type standalone, your CA DNS name, and an admin provisioner email. Move ca.json and secrets/ to /etc/step-ca, create a stepca system user, chmod 600 the secret keys, and enable a systemd unit pointing at ca.json with a password file.

step ca init creates a two-tier hierarchy: a root CA and an intermediate CA. The root signs the intermediate CSR, then stays offline. step-ca uses the intermediate key daily for leaf certificate signing. If the intermediate is compromised, you revoke it and issue a new one without replacing the root. Keep the root lifespan at 10 or more years and rotate the intermediate every one to two years. This is standard PKI practice wrapped into step-ca defaults.

Edit /etc/step-ca/ca.json and confirm an ACME-type provisioner exists, or add one with step ca provisioner add acme --type ACME. Restart step-ca after any config change. The ACME directory URL defaults to https://ca.internal.example.com/acme/acme/directory. Confirm it responds with curl and jq — you should see newNonce, newOrder, and newAccount endpoints. ACME clients then complete HTTP-01 or TLS-ALPN-01 validation before receiving certificates.

Install the root CA on the requesting machine first: copy root_ca.crt to /usr/local/share/ca-certificates/ and run update-ca-certificates. Then run certbot certonly --standalone with --server pointing at your step-ca ACME directory URL and your internal domain. Certbot stores certs under /etc/letsencrypt/live/ even though the issuer is internal. Wire those paths into nginx the same way you would for a public certificate. For one-off or CI use, step ca certificate with --ca-url and --root is faster than ACME.

Export root_ca.crt from initialization output and add it to every OS trust store: macOS Keychain, Windows certmgr, and Linux update-ca-certificates. Language runtimes often maintain separate stores — configure Java cacerts, NODE_EXTRA_CA_CERTS for Node.js, and REQUESTS_CA_BUNDLE for Python. Document the root fingerprint so engineers verify they imported the correct certificate. For CI pipelines, store the root as a base64-encoded secret and decode at job start. Without this step, browsers and services reject your internal chain.

Use short validity periods — 24 hours to 90 days, with 30 days or less recommended. Schedule certbot renew via cron on application servers, for example daily at 02:00 with a deploy-hook to reload nginx. Pair this with monitoring that alerts when a certificate expires in under seven days. On Kubernetes, point cert-manager ClusterIssuer at your step-ca ACME directory URL instead of shell scripts on every node. Treat cert expiry like disk space — predictable until it is not.

Treat the CA VM as a crown jewel. Keep the root private key offline after signing the intermediate. Restrict intermediate keys to chmod 600 owned by the stepca user. Enable CRL or OCSP before production cutover. Restrict CA API access to internal networks with UFW or security groups — deny public internet. Ship step-ca logs to a central stack and audit every issuance with requester, SAN list, and serial number. Do not expose the admin JWK provisioner to untrusted networks. At scale, use an HSM or cloud KMS for intermediate keys.

Let's Encrypt is free with excellent ACME automation but only works for public DNS names browsers already trust. Manual OpenSSL CAs suit one-off certs but carry high human error and no automation. AWS Private CA is API-driven and fits enterprise AWS shops but costs roughly Rs 15,000 or more per month (~USD 110). step-ca sits in the middle: one VM, low operational cost, ACME plus JWT plus SSH provisioners, and full control over validity periods and SAN lists for internal infrastructure.

Use step-ca for private DNS names, mTLS between services, VPN endpoints, SSH certificate provisioners, and staging environments that mirror production TLS behaviour. Commercial CAs charge per hostname and enforce rate limits. Internal names like grafana.ops.local do not belong on a public trust chain. Keep Let's Encrypt for customer-facing domains. On legal-tech portals and client portals I have maintained, matching TLS behaviour in staging catches certificate chain errors before release week.

Back up the entire /etc/step-ca directory including secrets/, ca.json, and the root CA certificate. Store encrypted backups off-site. Test restoration quarterly — a backup you have never restored is a guess. A lost intermediate key or corrupted ca.json stops all issuance until you recover or rebuild the hierarchy.

Yes. Run step-ca in a container with secrets mounted as volumes. Keep persistent storage for the CA database if you enable revocation tracking. On Kubernetes, cert-manager supports custom ACME directory URLs — point a ClusterIssuer at your step-ca endpoint and let it handle ingress TLS automatically. That pattern scales better than renewal shell scripts on every node.

Terminate TLS at nginx with a step-ca certificate and let Laravel see HTTP on localhost, trusting X-Forwarded-Proto. For mTLS between microservices, configure nginx ssl_verify_client and pass verified client cert details to PHP via headers. Public sites keep Let's Encrypt; internal staging and service-to-service calls use step-ca. On portals with document uploads and payment callbacks, matching TLS in every environment prevents surprises during release. Webhook integrations benefit the same way when you test callback URLs against real certificate chains locally.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: