# TLS Certificates in Webship: The Embedded ACME CA
A TLS certificate does two jobs: it helps encrypt a connection and tells the client which identity it is talking to. Encryption can be strong while the trust decision is wrong for the audience. That is why certificate automation must start with one question: who must trust this site?
Webship 1.4.0 makes that choice independently for every configured site. A public website can use a browser-trusted ACME certificate, an internal service can use Webship’s embedded private certificate authority, and a site with an existing PKI can keep operator-managed certificate files. They can all share one Webship process without sharing one private key or one trust boundary.
Four automatic certificate modes, selected per site
The certificate_mode field belongs to each [[sites]] entry. It is not a global switch.
| Mode | Trust source | Best fit | Validation path | | --- | --- | --- | --- | | per_site | Public browser and operating-system trust stores | A public site with one exact hostname | Public ACME with TLS-ALPN-01 | | fleet | Public browser and operating-system trust stores | Large sets of third- and fourth-level names under explicit registered domains | Public ACME with DNS-01 and stable certificate shards | | embedded | A private Webship root installed by the operator | Internal services, managed devices, private fleets, and test environments | In-process issuance; no external challenge | | shared | Public browser and operating-system trust stores | Legacy deployments that intentionally use one public multi-SAN group | Public ACME with TLS-ALPN-01 |
The default is per_site. It orders one public certificate for the site’s exact name. Fleet mode is the scalable public option for many deep subdomains. Embedded mode uses Webship’s private in-process CA. Shared mode remains available for compatibility, but it is not the default.
A complete certificate and key under [sites.tls] always takes precedence over automatic issuance for that site.
What “embedded ACME CA” means
The configuration section is named [acme_ca], but the embedded CA is not a public or network-accessible ACME service. It exposes no directory endpoint, accepts no remote enrollment, calls no registrar API, and performs no proof-of-control challenge.
Instead, Webship keeps the entire private issuance path in one process:
- The site selects certificate_mode = "embedded".
- Webship loads or creates the private root identity in the configured state directory.
- Webship generates a new private key for the site.
- The embedded root signs a leaf certificate for that exact name.
- Webship validates the completed identity before installing it in the live TLS resolver.
- The certificate is then available to every enabled protocol for that site.
An ACME-style network challenge would only prove Webship to itself, so the embedded path deliberately has no network protocol. The [acme_ca] section is private PKI state: it defines where the root lives and how long issued leaf certificates remain valid.
Configure an embedded certificate for one site
This is the minimal shape for a private site:
~~~toml listen = "0.0.0.0:443"
[tls] unknown_sni = "reject"
[automatic_tls] enabled = true cache_dir = "/var/lib/webship/acme"
[acme_ca] state_dir = "/var/lib/webship/acme-ca" leaf_validity_days = 90
[[sites]] domain = "service.internal.example" root = "/srv/service" certificate_mode = "embedded"
[sites.protocols] h1 = true h2 = true h3 = true ~~~
The root identity is created lazily when an embedded site first needs it. Webship persists the root key with restrictive permissions in state_dir. The root certificate has a ten-year lifetime; leaf lifetime is controlled by leaf_validity_days.
Treat both storage locations as production state:
- The ACME cache holds automatically managed site identities.
- The embedded-CA state directory holds the private root identity.
- The service account needs access, but application users do not.
- Backups must preserve confidentiality and file permissions.
- Production, development, and testing should use separate roots and separate directories.
Deleting the root directory does not “reset TLS.” It creates a new trust anchor. Clients that trust the old root will reject certificates issued by the replacement until their trust stores are updated.
Private trust is intentional
Certificates from the embedded CA are not trusted automatically by public browsers or operating systems. They become trusted only after the operator installs the exported Webship root certificate in the client’s trust store.
That makes embedded mode a good fit for:
- company-managed laptops and phones enrolled through device management;
- internal service-to-service traffic with an explicit CA bundle;
- private appliances and controlled edge fleets;
- development and test environments that must exercise real TLS behavior;
- disconnected networks that cannot depend on a public CA.
It is not the right mode for an ordinary public website whose visitors use unmanaged browsers. Use public per_site issuance for an exact public name, fleet issuance for large public subdomain sets, shared only for a deliberate legacy multi-SAN deployment, or manual files from an already trusted PKI.
Distribute only the root certificate to clients. Never distribute the root private key. Possession of that key gives its holder authority to issue identities trusted by every enrolled client.
Public and private certificates can coexist
Webship 1.4.0 can mix certificate strategies on the same listener:
~~~toml listen = "0.0.0.0:443"
[tls] unknown_sni = "reject"
[automatic_tls] enabled = true directory_url = "https://acme-v02.api.letsencrypt.org/directory" cache_dir = "/var/lib/webship/acme" contacts = ["mailto:ops@example.com"] accept_terms_of_service = true
[acme_ca] state_dir = "/var/lib/webship/acme-ca" leaf_validity_days = 90
[[sites]] domain = "www.example.com" root = "/srv/public" certificate_mode = "per_site"
[[sites]] domain = "control.internal.example" root = "/srv/control" certificate_mode = "embedded"
[[sites]] domain = "payments.example.com" root = "/srv/payments"
[sites.tls] cert = "/etc/webship/payments-fullchain.pem" key = "/etc/webship/payments-private-key.pem" ~~~
Here, www.example.com receives its own public ACME certificate. control.internal.example receives a private certificate from the embedded CA. payments.example.com remains under the operator’s external PKI because its explicit files take precedence.
The public ACME directory is ignored by embedded sites. The embedded root never signs the public site. The manual site is never silently enrolled into either automatic workflow.
One certificate resolver for H1, H2, H3, and WebTransport
Certificate selection happens during the TLS handshake, before an HTTP request exists. Webship uses the ClientHello server name to select the site identity and then negotiates the application protocol.
- HTTP/1.1 and HTTP/2 use TLS over TCP.
- HTTP/3 and WebTransport use TLS inside QUIC over UDP.
- One valid site identity can serve every enabled protocol.
- HTTP/3 also requires UDP reachability; H1 and H2 use the TCP path.
- Alt-Svc can advertise H3 while preserving a TCP fallback.
TCP TLS and QUIC use the same site-aware identity model. Exact names take precedence, the longest valid wildcard wins where wildcard certificates are configured, and unknown named SNI can be rejected instead of receiving an unrelated default certificate.
Use unknown_sni = "reject" on a multi-site listener when an unrecognized hostname must fail closed. Test recognized names, unrecognized names, and your expected no-SNI behavior before production rollout.
Rotate embedded identities without a serving gap
Webship exposes certificate state and controlled mutations through its authenticated, loopback-bound MCP server:
- webship.tls.get_status reports the active certificate resolver and renewal state.
- webship.tls.reissue_certificate immediately reissues an automatically managed site only when that site uses embedded mode.
- webship.tls.reload reloads certificate state through the normal guarded TLS path.
- webship.acme_ca.status reports whether the private CA is selected, its state directory, leaf lifetime, issuance count, revocation count, and recent domain sample.
- webship.sites.apply adds or removes sites against a pinned configuration version.
For an embedded reissue, Webship creates and validates the replacement before swapping it into service. The current valid identity keeps serving until the new identity is ready. The retired identity is recorded only after the replacement is installed.
The immediate reissue operation intentionally rejects public per_site certificates. Public renewal must remain inside the public ACME lifecycle rather than being confused with private in-process signing. Shared-mode membership is also restart-frozen because changing a multi-SAN group rebuilds the identity boundary.
MCP is a privileged control surface. Keep it on loopback, require TLS and a strong bearer token, use an authenticated tunnel for remote administration, and audit every mutation.
Failure boundaries that matter
A secure certificate system must fail in the correct direction.
- A newly configured embedded site does not receive another site’s identity while issuance is pending.
- An invalid replacement is not installed over a working certificate.
- Explicit manual files prevent automatic ownership of that site.
- Unknown named SNI can be rejected before HTTP routing.
- The embedded CA remains private and has no remote enrollment endpoint.
- Public and embedded identities use separate cache paths inside the automatic-TLS state.
A warning that the embedded CA is not initialized means Webship could not arm the configured state directory. Fix ownership, permissions, persistence, or storage availability before sending traffic to the affected site. Do not work around the error by copying another environment’s root key.
Production checklist
Before enabling embedded mode:
- Identify every client population that must trust the site.
- Create a controlled process for exporting and installing the root certificate.
- Use separate root state for production, development, and testing.
- Persist and protect the embedded-CA state directory and automatic-TLS cache.
- Run Webship under a dedicated service account with access only to required key material.
- Select certificate_mode on every site whose trust boundary must be explicit.
- Set and test the unknown-SNI policy.
- Enable H1, H2, and H3 deliberately and verify both TCP and UDP paths.
- Exercise reissue, restart, backup, restore, and client-trust validation outside production.
- Run webship --check-config before rollout, then verify issuer, names, validity, chain, and negotiated protocols from a real client.
Choose trust first, automation second
The embedded CA removes an external certificate-service dependency for private infrastructure. It does not make a private root globally trusted, and it does not remove the operator’s PKI responsibilities.
Webship automates key generation, signing, validation, installation, rotation, and protocol-wide certificate selection. The operator still owns root custody, client enrollment, environment separation, backup, recovery, and the decision to use a public or private trust path.
That separation is the feature. A self-contained server can automate private TLS without pretending to be a public CA—and public sites can still use browser-trusted per-site or fleet issuance in the same process.
Read the versioned Webship 1.4.0 documentation before rollout. RFC 5280 defines certificate profiles and validation, RFC 6066 defines TLS server-name signaling, RFC 8446 defines TLS 1.3, RFC 8555 defines public ACME, and RFC 9525 defines service identity verification.