| 1 | --- |
| 2 | title: Certificate authorities |
| 3 | description: Select a CA, configure external account binding, and understand account identity and key storage. |
| 4 | --- |
| 5 | |
| 6 | An `ACME.Account` is the identity used to sign orders with one certificate |
| 7 | authority (CA). Certificates reference that account; changing the issuing account |
| 8 | replaces the dependent certificate resource. |
| 9 | |
| 10 | ## Choose a directory |
| 11 | |
| 12 | | Preset | Intended use | External account binding | |
| 13 | | --- | --- | --- | |
| 14 | | `ACME.LetsEncryptStaging` | Development; untrusted certificates | No | |
| 15 | | `ACME.LetsEncrypt` | Publicly trusted production certificates | No | |
| 16 | | `ACME.ZeroSSL` | ZeroSSL ACME issuance | Required | |
| 17 | | `ACME.GoogleTrustServicesStaging` | Google Trust Services staging | Required | |
| 18 | | `ACME.GoogleTrustServices` | Google Trust Services production | Required | |
| 19 | |
| 20 | A preset selects a directory URL, not an Alchemy subscription. CA eligibility, |
| 21 | issuance limits, validity periods, and terms still apply. Check the CA's current |
| 22 | requirements before deploying. The supplied Google presets do not imply that a |
| 23 | Google account or external binding credentials are provisioned for you. |
| 24 | |
| 25 | ## Start with staging |
| 26 | |
| 27 | Inside the stack from [Getting started](/acme/getting-started): |
| 28 | |
| 29 | ```typescript |
| 30 | const account = yield* ACME.Account("Issuer", { |
| 31 | ca: ACME.LetsEncryptStaging, |
| 32 | contact: ["mailto:ops@example.com"], |
| 33 | termsOfServiceAgreed: true, |
| 34 | }); |
| 35 | ``` |
| 36 | |
| 37 | Staging exercises real account registration and DNS-01 validation. Its certificate |
| 38 | chain is intentionally untrusted; a browser warning is expected even after a |
| 39 | successful deployment. |
| 40 | |
| 41 | ## Switch to production |
| 42 | |
| 43 | ```diff lang="typescript" |
| 44 | const account = yield* ACME.Account("Issuer", { |
| 45 | - ca: ACME.LetsEncryptStaging, |
| 46 | + ca: ACME.LetsEncrypt, |
| 47 | ``` |
| 48 | |
| 49 | Changing the directory replaces the account and reissues dependent certificates. |
| 50 | Use a separate production stage and state store when development certificates |
| 51 | must remain independent. Schedule [renewal](/acme/renewal) before deploying a |
| 52 | production certificate to a live service. |
| 53 | |
| 54 | ## Configure ZeroSSL external account binding |
| 55 | |
| 56 | External account binding (EAB) associates an ACME account with a CA customer |
| 57 | account. Obtain a matching key ID and HMAC key from ZeroSSL and put them in your |
| 58 | secret configuration as `ZEROSSL_EAB_KID` and `ZEROSSL_EAB_HMAC_KEY`. |
| 59 | |
| 60 | ```typescript |
| 61 | import * as ACME from "alchemy/ACME"; |
| 62 | import * as Config from "effect/Config"; |
| 63 | |
| 64 | export const Issuer = ACME.Account("Issuer", { |
| 65 | ca: ACME.ZeroSSL, |
| 66 | eab: { |
| 67 | keyId: Config.String("ZEROSSL_EAB_KID"), |
| 68 | hmacKey: Config.Redacted("ZEROSSL_EAB_HMAC_KEY"), |
| 69 | }, |
| 70 | termsOfServiceAgreed: true, |
| 71 | }); |
| 72 | ``` |
| 73 | |
| 74 | The HMAC key is the CA-issued base64url value, not your ZeroSSL REST API key. |
| 75 | Keep the pair together. Yield `Issuer` in the stack or reference it from a |
| 76 | certificate to register it. Google Trust Services uses the same `eab` shape with |
| 77 | credentials obtained from Google. |
| 78 | |
| 79 | ## Reuse an existing account resource |
| 80 | |
| 81 | ```typescript |
| 82 | const account = yield* Issuer; |
| 83 | const certificate = yield* ACME.Certificate("Api", { |
| 84 | account, |
| 85 | identifiers: ["api.example.com"], |
| 86 | solver: Cloudflare.DNS.AcmeSolver({ |
| 87 | zoneId: Config.String("CLOUDFLARE_ZONE_ID"), |
| 88 | }), |
| 89 | }); |
| 90 | ``` |
| 91 | |
| 92 | Referencing the same account resource reuses its persisted signing key and account |
| 93 | URL. EAB is used when registering the account; ordinary signed requests use the |
| 94 | account key. Changing the EAB key ID replaces the account. Changing only the HMAC |
| 95 | value does not force an existing account to register again. |
| 96 | |
| 97 | ## Choose an account key algorithm |
| 98 | |
| 99 | ```diff lang="typescript" |
| 100 | export const Issuer = ACME.Account("Issuer", { |
| 101 | ca: ACME.ZeroSSL, |
| 102 | + keyAlgorithm: "RS256", |
| 103 | ``` |
| 104 | |
| 105 | The default is `ES256`; `RS256` is also supported. Changing the account algorithm |
| 106 | replaces the account. A certificate's separate `keyAlgorithm` controls the key |
| 107 | used by your TLS server, not the account signing key. |
| 108 | |
| 109 | ## Use a custom directory |
| 110 | |
| 111 | ```typescript |
| 112 | export const PrivateIssuer = ACME.Account("PrivateIssuer", { |
| 113 | ca: { |
| 114 | directoryUrl: "https://ca.internal.example/acme/acme/directory", |
| 115 | }, |
| 116 | termsOfServiceAgreed: true, |
| 117 | }); |
| 118 | ``` |
| 119 | |
| 120 | Use this form for another ACME-compatible CA. It must offer DNS-01 challenges for |
| 121 | the requested identifiers. Network access to the directory and authorization |
| 122 | endpoints must be available wherever issuance runs. |
| 123 | |
| 124 | ## Trust a private CA's HTTPS endpoint |
| 125 | |
| 126 | ```diff lang="typescript" |
| 127 | ca: { |
| 128 | directoryUrl: "https://ca.internal.example/acme/acme/directory", |
| 129 | + trustedRoot: Config.String("ACME_CA_ROOT_PEM"), |
| 130 | }, |
| 131 | ``` |
| 132 | |
| 133 | `trustedRoot` is a PEM trust anchor for the CA's **API connection**, not the chain |
| 134 | that your application serves. The current HTTP implementation supplies Bun's |
| 135 | `fetch` TLS option; other runtimes may ignore it. Use Bun for this private-root |
| 136 | path, or arrange platform-level trust and verify connectivity in your target |
| 137 | runtime. It does not disable certificate verification. |
| 138 | |
| 139 | ## Preserve account identity |
| 140 | |
| 141 | Alchemy generates the account key and stores it in stack state. `AccountProps` |
| 142 | does not currently accept an existing account key or account URL, so importing an |
| 143 | externally created account is not supported by this resource. The lower-level |
| 144 | Distilled SDK can accept explicit credentials, but it does not import them into |
| 145 | Alchemy's resource lifecycle. |
| 146 | |
| 147 | Back up state securely. Recreating a lost state file generates a new key rather |
| 148 | than recovering the old account. A crash after CA registration but before the |
| 149 | first state save can likewise leave an account whose key was not persisted. |
| 150 | `Redacted` protects ordinary logs, not the storage backend. |
| 151 | |
| 152 | ## Next steps |
| 153 | |
| 154 | - [DNS validation](/acme/dns-validation) — prove control of the requested names. |
| 155 | - [Runtime issuance](/acme/runtime) — bind this account into an application. |
| 156 | - [Account reference](/providers/acme/account) — all account props and outputs. |