zuplo 6.70.24 → 6.70.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/articles/api-key-consumer-bucket-portal-ui.mdx +223 -0
- package/docs/articles/monetization/api-access.mdx +3 -3
- package/docs/articles/monetization/subscription-lifecycle.md +1 -1
- package/docs/articles/securing-backend-mtls.mdx +1 -1
- package/docs/articles/securing-the-gateway-with-client-mtls.mdx +318 -0
- package/docs/concepts/authentication.mdx +2 -2
- package/docs/policies/mtls-auth-inbound/intro.md +7 -6
- package/docs/policies/mtls-auth-inbound/schema.json +15 -2
- package/package.json +4 -4
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Create Consumers in a Specific Bucket
|
|
3
|
+
sidebar_label: Consumers in a Specific Bucket
|
|
4
|
+
description:
|
|
5
|
+
Learn how to create API key consumers in a specific bucket using the Zuplo
|
|
6
|
+
portal UI, including bucket selection, environment mapping, and
|
|
7
|
+
troubleshooting.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Every API key consumer in Zuplo lives inside a **bucket**, and each bucket is
|
|
11
|
+
scoped to a specific environment. This guide shows how to pick a target bucket
|
|
12
|
+
from the **Services** screen and create a consumer inside it.
|
|
13
|
+
|
|
14
|
+
:::note
|
|
15
|
+
|
|
16
|
+
For general API key management (creating consumers, viewing keys, assigning
|
|
17
|
+
managers), see [Manage Keys in the Portal](./api-key-administration.mdx).
|
|
18
|
+
|
|
19
|
+
:::
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- A Zuplo project with at least one deployed environment (see the
|
|
24
|
+
[getting started tutorial](./step-1-setup-basic-gateway.mdx))
|
|
25
|
+
- The [API Key Authentication policy](../policies/api-key-inbound.mdx)
|
|
26
|
+
configured on your routes
|
|
27
|
+
- Permission to manage API key consumers in your project
|
|
28
|
+
|
|
29
|
+
## Understanding buckets and environments
|
|
30
|
+
|
|
31
|
+
Zuplo creates three buckets for every project. Each isolates its own consumers
|
|
32
|
+
and keys, so a key created in one bucket only authenticates requests against the
|
|
33
|
+
matching environment.
|
|
34
|
+
|
|
35
|
+
| Bucket | Environment | Git branch |
|
|
36
|
+
| --------------- | ----------- | -------------------- |
|
|
37
|
+
| **Production** | Production | Default branch |
|
|
38
|
+
| **Preview** | Preview | Non-default branches |
|
|
39
|
+
| **Development** | Development | Local development |
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
For deeper detail, see [Buckets and Environments](./api-key-buckets.mdx).
|
|
44
|
+
|
|
45
|
+
### When you need a non-default bucket
|
|
46
|
+
|
|
47
|
+
- **Per-environment isolation.** Keep staging keys out of production.
|
|
48
|
+
- **Custom buckets.** Your team created extra buckets (QA, per-tenant) via the
|
|
49
|
+
[Developer API](./api-key-api.mdx).
|
|
50
|
+
- **Shared buckets across projects.** Enterprise setups where one bucket backs
|
|
51
|
+
several projects.
|
|
52
|
+
|
|
53
|
+
## Find your buckets in the portal
|
|
54
|
+
|
|
55
|
+
<Stepper>
|
|
56
|
+
|
|
57
|
+
1. Open your project in the [Zuplo Portal](https://portal.zuplo.com).
|
|
58
|
+
|
|
59
|
+
1. Navigate to the
|
|
60
|
+
[**Services**](https://portal.zuplo.com/+/account/project/services) page.
|
|
61
|
+
|
|
62
|
+
1. Locate the **API Key Service** card. Use the **environment dropdown** at the
|
|
63
|
+
top right to filter the visible buckets. Pick **All Environments** to see
|
|
64
|
+
every bucket, or pick a single environment to narrow the list.
|
|
65
|
+
|
|
66
|
+
</Stepper>
|
|
67
|
+
|
|
68
|
+
<ModalScreenshot size="md">
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
</ModalScreenshot>
|
|
73
|
+
|
|
74
|
+
The card's **Connected to** badge shows which environment's bucket is currently
|
|
75
|
+
active.
|
|
76
|
+
|
|
77
|
+
## Create a consumer in a specific bucket
|
|
78
|
+
|
|
79
|
+
<Stepper>
|
|
80
|
+
|
|
81
|
+
1. On the **Services** page, choose the target environment from the dropdown.
|
|
82
|
+
|
|
83
|
+
1. On the API Key Service card, click **Bucket Details**. The bucket's consumer
|
|
84
|
+
list opens.
|
|
85
|
+
|
|
86
|
+
1. Click **Create new consumer** and fill in the form below.
|
|
87
|
+
|
|
88
|
+
1. Click **Save consumer**, then confirm it appears in the list.
|
|
89
|
+
|
|
90
|
+
</Stepper>
|
|
91
|
+
|
|
92
|
+
<ModalScreenshot size="md">
|
|
93
|
+
|
|
94
|
+

|
|
95
|
+
|
|
96
|
+
</ModalScreenshot>
|
|
97
|
+
|
|
98
|
+
### Consumer form fields
|
|
99
|
+
|
|
100
|
+
| Field | Required | Runtime value | Notes |
|
|
101
|
+
| ---------------- | -------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| **Subject** | Yes | `request.user.sub` | Unique within the bucket. Identifies the consumer in logs and policy code. |
|
|
103
|
+
| **Key managers** | No | n/a | Comma-separated emails of users who can manage this consumer's keys via the [Developer Portal](../dev-portal/introduction.mdx). |
|
|
104
|
+
| **Metadata** | No | `request.user.data` | Valid JSON object. Plan info, customer IDs, anything your policies need at runtime. |
|
|
105
|
+
|
|
106
|
+
:::tip
|
|
107
|
+
|
|
108
|
+
Once created, the consumer's API key only authenticates requests routed through
|
|
109
|
+
an environment whose API Key Authentication policy resolves to that same bucket.
|
|
110
|
+
|
|
111
|
+
:::
|
|
112
|
+
|
|
113
|
+
## How bucket selection affects key validation
|
|
114
|
+
|
|
115
|
+
The [API Key Authentication policy](../policies/api-key-inbound.mdx) decides
|
|
116
|
+
which bucket to validate keys against. With no `bucketName` set, the policy
|
|
117
|
+
defaults to the bucket that matches the current environment:
|
|
118
|
+
|
|
119
|
+
| Environment | Default bucket |
|
|
120
|
+
| ----------- | -------------- |
|
|
121
|
+
| Production | Production |
|
|
122
|
+
| Preview | Preview |
|
|
123
|
+
| Development | Development |
|
|
124
|
+
|
|
125
|
+
For a custom bucket, set `bucketName` (or `bucketId`) on the policy so it checks
|
|
126
|
+
the right one:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"export": "ApiKeyInboundPolicy",
|
|
131
|
+
"module": "$import(@zuplo/runtime)",
|
|
132
|
+
"options": {
|
|
133
|
+
"bucketName": "my-custom-bucket",
|
|
134
|
+
"allowUnauthenticatedRequests": false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
:::caution
|
|
140
|
+
|
|
141
|
+
If the consumer lives in one bucket but the policy checks a different bucket,
|
|
142
|
+
the key is not found and the request returns `401 Unauthorized`. Make sure the
|
|
143
|
+
policy's bucket matches the bucket where you created the consumer.
|
|
144
|
+
|
|
145
|
+
:::
|
|
146
|
+
|
|
147
|
+
## Using the Developer API instead
|
|
148
|
+
|
|
149
|
+
To script consumer creation as part of an onboarding flow or CI/CD pipeline, use
|
|
150
|
+
the [Zuplo Developer API](./api-key-api.mdx):
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
curl \
|
|
154
|
+
https://dev.zuplo.com/v1/accounts/$ACCOUNT_NAME/key-buckets/$BUCKET_NAME/consumers?with-api-key=true \
|
|
155
|
+
--request POST \
|
|
156
|
+
--header "Content-type: application/json" \
|
|
157
|
+
--header "Authorization: Bearer $ZAPI_KEY" \
|
|
158
|
+
--data '{
|
|
159
|
+
"name": "my-consumer",
|
|
160
|
+
"description": "Created via API",
|
|
161
|
+
"metadata": { "plan": "gold" }
|
|
162
|
+
}'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Replace `$ACCOUNT_NAME` with your Zuplo account name, `$BUCKET_NAME` with the
|
|
166
|
+
target bucket name, and `$ZAPI_KEY` with your
|
|
167
|
+
[Zuplo API key](./accounts/zuplo-api-keys.mdx). Full reference at the
|
|
168
|
+
[Developer API documentation](https://dev.zuplo.com/docs).
|
|
169
|
+
|
|
170
|
+
## Troubleshooting
|
|
171
|
+
|
|
172
|
+
<details>
|
|
173
|
+
<summary>My API key returns 401 Unauthorized</summary>
|
|
174
|
+
|
|
175
|
+
Usually a bucket mismatch. The consumer is in one bucket, but the policy checks
|
|
176
|
+
a different one.
|
|
177
|
+
|
|
178
|
+
1. In the portal, navigate to **Services** and confirm which bucket holds the
|
|
179
|
+
consumer.
|
|
180
|
+
2. Open the route's API Key Authentication policy. If `bucketName` or `bucketId`
|
|
181
|
+
is set, verify it matches the consumer's bucket. If neither is set, the
|
|
182
|
+
policy uses the current environment's default bucket.
|
|
183
|
+
3. Either recreate the consumer in the correct bucket, or update the policy's
|
|
184
|
+
`bucketName` to match.
|
|
185
|
+
|
|
186
|
+
</details>
|
|
187
|
+
|
|
188
|
+
<details>
|
|
189
|
+
<summary>I don't see the bucket I'm looking for</summary>
|
|
190
|
+
|
|
191
|
+
- **Environment filter.** Set the dropdown to **All Environments** to see every
|
|
192
|
+
bucket.
|
|
193
|
+
- **Custom buckets.** Buckets created via the Developer API are account-scoped,
|
|
194
|
+
not environment-scoped. They show under **All Environments**. If still
|
|
195
|
+
missing, confirm the account using the
|
|
196
|
+
[list buckets API endpoint](https://dev.zuplo.com/docs).
|
|
197
|
+
- **Permissions.** Account-level roles control access to the Services page.
|
|
198
|
+
Confirm your role can view and manage API key consumers.
|
|
199
|
+
|
|
200
|
+
</details>
|
|
201
|
+
|
|
202
|
+
<details>
|
|
203
|
+
<summary>I created a consumer but it doesn't appear in the expected environment</summary>
|
|
204
|
+
|
|
205
|
+
Consumers belong to buckets, not environments directly. A consumer created while
|
|
206
|
+
viewing the **Preview** environment sits in the preview bucket and only
|
|
207
|
+
authenticates preview environments. Switch the dropdown to **All Environments**
|
|
208
|
+
or the specific environment to locate it.
|
|
209
|
+
|
|
210
|
+
</details>
|
|
211
|
+
|
|
212
|
+
## Related documentation
|
|
213
|
+
|
|
214
|
+
- [Buckets and Environments](./api-key-buckets.mdx): How buckets map to
|
|
215
|
+
environments
|
|
216
|
+
- [Manage Keys in the Portal](./api-key-administration.mdx): General portal
|
|
217
|
+
management walkthrough
|
|
218
|
+
- [API Key Authentication policy](../policies/api-key-inbound.mdx): Policy
|
|
219
|
+
configuration reference including `bucketName`
|
|
220
|
+
- [Use the Developer API](./api-key-api.mdx): Programmatic consumer management
|
|
221
|
+
- [Create an API Key Consumer on Login](../dev-portal/dev-portal-create-consumer-on-auth.mdx):
|
|
222
|
+
Automatically create consumers when users sign in
|
|
223
|
+
- [Environments](./environments.mdx): How environments work in Zuplo
|
|
@@ -65,7 +65,7 @@ curl \
|
|
|
65
65
|
|
|
66
66
|
Each bucket has an optional `MonetizationConfiguration` record that holds
|
|
67
67
|
bucket-wide defaults. The configuration is read by the runtime and the Developer
|
|
68
|
-
Portal
|
|
68
|
+
Portal.
|
|
69
69
|
|
|
70
70
|
| Method | Path |
|
|
71
71
|
| -------- | ---------------------------------------------------- |
|
|
@@ -105,8 +105,8 @@ the schema defaults (`multipleSubscriptionsEnabled: false`, `planOrder: []`,
|
|
|
105
105
|
|
|
106
106
|
These endpoints script the Stripe integration that the
|
|
107
107
|
[Monetization Service UI](./stripe-integration.md#connecting-your-stripe-account)
|
|
108
|
-
runs interactively. They live on the Zuplo developer API
|
|
109
|
-
|
|
108
|
+
runs interactively. They live on the Zuplo developer API, so the request shape
|
|
109
|
+
is documented here.
|
|
110
110
|
|
|
111
111
|
### Connect a Stripe app
|
|
112
112
|
|
|
@@ -82,7 +82,7 @@ curl -X POST https://dev.zuplo.com/v3/metering/{bucketId}/subscriptions \
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
`plan` references the target plan by its `key` (and optionally `version`).
|
|
85
|
-
Provide either `customerId` (the
|
|
85
|
+
Provide either `customerId` (the Zuplo customer ULID, format
|
|
86
86
|
`^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$`) or `customerKey` (your own
|
|
87
87
|
identifier). Optional fields include `timing` (`"immediate"` by default,
|
|
88
88
|
`"next_billing_cycle"`, or an RFC 3339 timestamp), `startingPhase`, `name`,
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Client mTLS Authentication
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<EnterpriseFeature name="Client mTLS" />
|
|
6
|
+
|
|
7
|
+
Client mTLS authentication lets your Zuplo gateway verify the identity of
|
|
8
|
+
clients calling your API using certificates issued by your own Certificate
|
|
9
|
+
Authority (CA). Both the client and the gateway authenticate each other during
|
|
10
|
+
the TLS handshake, so only clients holding a certificate signed by your CA can
|
|
11
|
+
reach your API.
|
|
12
|
+
|
|
13
|
+
## How Client mTLS Works
|
|
14
|
+
|
|
15
|
+
When a client calls your Zuplo gateway:
|
|
16
|
+
|
|
17
|
+
1. The client presents a certificate issued by your CA during the TLS handshake.
|
|
18
|
+
2. Zuplo's edge verifies the certificate against the CA you've uploaded and
|
|
19
|
+
passes the verification result (and parsed certificate) to your gateway
|
|
20
|
+
workers.
|
|
21
|
+
3. The [`mtls-auth-inbound`](../policies/mtls-auth-inbound.md) policy on your
|
|
22
|
+
route reads the verification result, enforces it, and attaches the parsed
|
|
23
|
+
certificate metadata to `request.user.data.mtlsAuth` for use in your handlers
|
|
24
|
+
and downstream policies.
|
|
25
|
+
|
|
26
|
+
CA certificates are scoped to your Zuplo **account**, not a single project or
|
|
27
|
+
deployment. Once a CA is uploaded, every gateway domain on the account will
|
|
28
|
+
verify presented client certificates against it. The policy on each route
|
|
29
|
+
controls whether unverified traffic is rejected or allowed through.
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
Before you begin, you need:
|
|
34
|
+
|
|
35
|
+
- A public CA certificate (PEM-encoded) that issued — or will issue — the client
|
|
36
|
+
certificates you want to accept
|
|
37
|
+
- The [Zuplo CLI](../cli/overview.mdx) installed and authenticated
|
|
38
|
+
- A Zuplo project where you can add the `mtls-auth-inbound` policy to a route
|
|
39
|
+
|
|
40
|
+
:::note
|
|
41
|
+
|
|
42
|
+
You only upload your **public CA certificate** to Zuplo. Private keys and issued
|
|
43
|
+
client certificates stay with you and your clients.
|
|
44
|
+
|
|
45
|
+
:::
|
|
46
|
+
|
|
47
|
+
## 1/ Upload Your CA Certificate
|
|
48
|
+
|
|
49
|
+
Use the Zuplo CLI to upload your CA certificate. The CA is registered against
|
|
50
|
+
your account and is automatically made available on all of your gateway domains.
|
|
51
|
+
|
|
52
|
+
First, authenticate your client:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
zuplo login
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then you can create a CA by running:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
zuplo ca-certificate create \
|
|
62
|
+
--name my_ca \
|
|
63
|
+
--cert ./ca.pem \
|
|
64
|
+
--account your-account
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Parameters:**
|
|
68
|
+
|
|
69
|
+
- `--name`: A unique identifier for the CA. Must be a valid JavaScript
|
|
70
|
+
identifier (letters, digits, `_`, `$`; cannot start with a digit).
|
|
71
|
+
- `--cert`: Path to the PEM-encoded CA certificate
|
|
72
|
+
(`-----BEGIN CERTIFICATE-----` ...). DER is not supported.
|
|
73
|
+
- `--account`: Your Zuplo account name.
|
|
74
|
+
|
|
75
|
+
The command returns the new CA's ID (prefixed with `mtlsca_`). You can list all
|
|
76
|
+
CAs on the account at any time:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
zuplo ca-certificate list --account your-account
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
See the [`ca-certificate` CLI reference](../cli/ca-certificate-create.md) for
|
|
83
|
+
all available subcommands (`create`, `list`, `describe`, `update`, `delete`).
|
|
84
|
+
|
|
85
|
+
:::tip{title="Using an intermediate CA"}
|
|
86
|
+
|
|
87
|
+
If your client certificates are issued by an intermediate CA (rather than
|
|
88
|
+
directly by your root), upload the **intermediate** itself as the CA — not the
|
|
89
|
+
root. The client certs used must be directly signed by the CA certificate you
|
|
90
|
+
provide to Zuplo.
|
|
91
|
+
|
|
92
|
+
:::
|
|
93
|
+
|
|
94
|
+
## 2/ Add the mTLS Auth Inbound Policy
|
|
95
|
+
|
|
96
|
+
Add the [`mtls-auth-inbound`](../policies/mtls-auth-inbound.md) policy to any
|
|
97
|
+
route that should require a verified client certificate. The policy reads the
|
|
98
|
+
verification result that Zuplo's edge attached to the request and either rejects
|
|
99
|
+
unverified traffic or allows it through, depending on configuration.
|
|
100
|
+
|
|
101
|
+
```json title="config/policies.json"
|
|
102
|
+
{
|
|
103
|
+
"name": "my-mtls-auth-inbound-policy",
|
|
104
|
+
"policyType": "mtls-auth-inbound",
|
|
105
|
+
"handler": {
|
|
106
|
+
"export": "MTLSAuthInboundPolicy",
|
|
107
|
+
"module": "$import(@zuplo/runtime)",
|
|
108
|
+
"options": {
|
|
109
|
+
"allowUnauthenticatedRequests": false,
|
|
110
|
+
"certIssuerDN": "CN=example-ca, O=Example, C=US"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Key options:**
|
|
117
|
+
|
|
118
|
+
- `allowUnauthenticatedRequests` (default `false`): When `false`, the policy
|
|
119
|
+
rejects requests that don't present a valid client certificate signed by a CA
|
|
120
|
+
on your account. When `true`, the policy lets traffic through but still
|
|
121
|
+
attaches certificate metadata when a parseable client certificate is present —
|
|
122
|
+
useful for staged rollouts or logging-only modes.
|
|
123
|
+
- `certIssuerDN`: The fully qualified issuer distinguished name that the client
|
|
124
|
+
certificate must be signed by.
|
|
125
|
+
|
|
126
|
+
See the full [policy reference](../policies/mtls-auth-inbound.md) for all
|
|
127
|
+
options.
|
|
128
|
+
|
|
129
|
+
:::tip{title="Finding your certIssuerDN value"}
|
|
130
|
+
|
|
131
|
+
The issuer DN of a client certificate is the subject DN of the CA that signed
|
|
132
|
+
it. Read it directly from your CA's PEM file with `openssl`:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
openssl x509 -in ca.pem -noout -subject -nameopt RFC2253
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This prints something like `subject=CN=example-ca,O=Example,C=US`. Copy the part
|
|
139
|
+
after `subject=` into `certIssuerDN`. The policy tolerates casing and whitespace
|
|
140
|
+
differences, but not RDN reordering, so keep the order produced by `openssl`
|
|
141
|
+
as-is.
|
|
142
|
+
|
|
143
|
+
:::
|
|
144
|
+
|
|
145
|
+
## 3/ Read Certificate Metadata in Your Handler
|
|
146
|
+
|
|
147
|
+
When verification succeeds, the policy attaches parsed certificate metadata to
|
|
148
|
+
`request.user.data.mtlsAuth`. If `request.user` does not already exist, the
|
|
149
|
+
policy also sets `request.user.sub` to the certificate subject.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
153
|
+
|
|
154
|
+
export default async function (request: ZuploRequest, context: ZuploContext) {
|
|
155
|
+
const mtls = request.user?.data?.mtlsAuth;
|
|
156
|
+
|
|
157
|
+
if (!mtls) {
|
|
158
|
+
return new Response("No client certificate", { status: 401 });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
context.log.info("Authenticated client", {
|
|
162
|
+
subject: mtls.subject,
|
|
163
|
+
issuer: mtls.issuer,
|
|
164
|
+
fingerprint: mtls.sha256Fingerprint,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Authorize based on certificate subject, fingerprint, etc.
|
|
168
|
+
return new Response(`Hello, ${mtls.subject}`);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The metadata object includes:
|
|
173
|
+
|
|
174
|
+
- `subject` — the client certificate subject DN
|
|
175
|
+
- `issuer` — the issuer DN (the CA that signed the certificate)
|
|
176
|
+
- `notBefore` / `notAfter` — validity window in ISO 8601 format
|
|
177
|
+
- `sha256Fingerprint` — SHA-256 digest of the DER-encoded certificate, uppercase
|
|
178
|
+
hex with colon separators (e.g. `AB:CD:EF:...`). Useful for pinning specific
|
|
179
|
+
client certificates.
|
|
180
|
+
|
|
181
|
+
The raw client certificate is also available on
|
|
182
|
+
`context.incomingRequestProperties.clientCert` in
|
|
183
|
+
[RFC 9440](https://datatracker.ietf.org/doc/html/rfc9440#section-2.2) format
|
|
184
|
+
(Base64-encoded DER, colon-wrapped) if you need to perform custom parsing or
|
|
185
|
+
forward it to a backend.
|
|
186
|
+
|
|
187
|
+
## 4/ Test with curl
|
|
188
|
+
|
|
189
|
+
Once your CA is uploaded and the policy is on the route, you can verify the
|
|
190
|
+
end-to-end flow with `curl`. You'll need a client certificate and private key
|
|
191
|
+
issued by the CA you uploaded.
|
|
192
|
+
|
|
193
|
+
Send the certificate and key with `--cert` and `--key`:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
curl --cert ./client.pem --key ./client.key \
|
|
197
|
+
https://your-gateway.zuplo.app/v1/example
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Confirm that:
|
|
201
|
+
|
|
202
|
+
- A request **without** `--cert` is rejected with `401` when
|
|
203
|
+
`allowUnauthenticatedRequests` is `false`.
|
|
204
|
+
- A request with a certificate signed by your uploaded CA succeeds and your
|
|
205
|
+
handler sees the parsed certificate on `request.user.data.mtlsAuth`.
|
|
206
|
+
- A request with a certificate signed by a different CA is rejected.
|
|
207
|
+
|
|
208
|
+
## Managing CA Certificates
|
|
209
|
+
|
|
210
|
+
### Listing CAs
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
zuplo ca-certificate list --account your-account
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Inspecting a CA
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
zuplo ca-certificate describe \
|
|
220
|
+
--cert-id mtlsca_abc123 \
|
|
221
|
+
--account your-account
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Renaming a CA
|
|
225
|
+
|
|
226
|
+
Only the name can be updated; to replace the certificate body, delete the CA and
|
|
227
|
+
create a new one.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
zuplo ca-certificate update \
|
|
231
|
+
--cert-id mtlsca_abc123 \
|
|
232
|
+
--name renamed_ca \
|
|
233
|
+
--account your-account
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Deleting a CA
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
zuplo ca-certificate delete \
|
|
240
|
+
--cert-id mtlsca_abc123 \
|
|
241
|
+
--account your-account
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
:::caution
|
|
245
|
+
|
|
246
|
+
Deleting a CA stops verification for client certificates issued by it on all of
|
|
247
|
+
your gateway domains. Routes that use `mtls-auth-inbound` with
|
|
248
|
+
`allowUnauthenticatedRequests: false` will start rejecting those clients
|
|
249
|
+
immediately. Rotate to a new CA and update your clients before deleting the old
|
|
250
|
+
CA.
|
|
251
|
+
|
|
252
|
+
:::
|
|
253
|
+
|
|
254
|
+
### Rotating a CA
|
|
255
|
+
|
|
256
|
+
To rotate the CA without downtime:
|
|
257
|
+
|
|
258
|
+
1. Upload the new CA alongside the existing one with
|
|
259
|
+
`zuplo ca-certificate create`.
|
|
260
|
+
2. Reissue client certificates from the new CA and distribute them to your
|
|
261
|
+
clients.
|
|
262
|
+
3. Once all clients have moved to the new CA, delete the old CA with
|
|
263
|
+
`zuplo ca-certificate delete`.
|
|
264
|
+
|
|
265
|
+
If you've followed the common practice of preserving the CA's subject DN across
|
|
266
|
+
the rotation (only the key, serial, and validity dates change), the issuer DN on
|
|
267
|
+
newly issued client certificates is identical to the previous one and
|
|
268
|
+
**`certIssuerDN` does not need to change**. If the rotation deliberately changes
|
|
269
|
+
the CA's subject DN, update `certIssuerDN` to match the new value before cutting
|
|
270
|
+
clients over — or temporarily set `allowUnauthenticatedRequests: true` to allow
|
|
271
|
+
both issuers during the transition.
|
|
272
|
+
|
|
273
|
+
## Local Development
|
|
274
|
+
|
|
275
|
+
The `mtls-auth-inbound` policy relies on verification metadata supplied by
|
|
276
|
+
Zuplo's edge proxy and does not work in local development with `zuplo dev`. Test
|
|
277
|
+
the policy in a working-copy or preview environment.
|
|
278
|
+
|
|
279
|
+
## Troubleshooting
|
|
280
|
+
|
|
281
|
+
### Requests are rejected with 401
|
|
282
|
+
|
|
283
|
+
- Confirm the client is presenting a certificate signed by a CA that's been
|
|
284
|
+
uploaded with `zuplo ca-certificate list`.
|
|
285
|
+
- If you've set `certIssuerDN`, verify it matches
|
|
286
|
+
`request.user.data.mtlsAuth.issuer` exactly (casing and whitespace are
|
|
287
|
+
tolerated, but RDN order is not).
|
|
288
|
+
- Temporarily set `allowUnauthenticatedRequests: true` and log
|
|
289
|
+
`context.incomingRequestProperties.clientMtlsVerificationStatus` and
|
|
290
|
+
`context.incomingRequestProperties.clientMtlsVerificationReason` to see why
|
|
291
|
+
verification failed.
|
|
292
|
+
|
|
293
|
+
### `request.user.data.mtlsAuth` is missing
|
|
294
|
+
|
|
295
|
+
- The policy only attaches metadata when a parseable client certificate is
|
|
296
|
+
present on the request. Confirm the client is sending one.
|
|
297
|
+
- Verify the route includes the `mtls-auth-inbound` policy.
|
|
298
|
+
|
|
299
|
+
### Custom domains
|
|
300
|
+
|
|
301
|
+
When a CA is uploaded, it's automatically associated with Zuplo's managed
|
|
302
|
+
gateway domains. A custom domain must be _active_ in the dashboard (check the
|
|
303
|
+
Settings/Custom Domains sidebar) before CA verification will become active on
|
|
304
|
+
that custom domain.
|
|
305
|
+
|
|
306
|
+
If you add a custom domain later and your clients aren't being verified against
|
|
307
|
+
it, contact [support@zuplo.com](mailto:support@zuplo.com).
|
|
308
|
+
|
|
309
|
+
## Additional Resources
|
|
310
|
+
|
|
311
|
+
- [`mtls-auth-inbound` policy reference](../policies/mtls-auth-inbound.md)
|
|
312
|
+
- [`ca-certificate` CLI reference](../cli/ca-certificate-create.md)
|
|
313
|
+
- [Gateway to Origin mTLS Authentication](./securing-backend-mtls.mdx) — the
|
|
314
|
+
reverse direction, where Zuplo authenticates to your backend with a client
|
|
315
|
+
certificate
|
|
316
|
+
|
|
317
|
+
If you need help configuring client mTLS for your account, contact us at
|
|
318
|
+
[support@zuplo.com](mailto:support@zuplo.com).
|
|
@@ -76,8 +76,8 @@ users already authenticate with an identity provider.
|
|
|
76
76
|
|
|
77
77
|
- [Basic Auth](../policies/basic-auth-inbound.mdx) - Username/password
|
|
78
78
|
authentication
|
|
79
|
-
- [mTLS](../
|
|
80
|
-
authentication
|
|
79
|
+
- [mTLS](../articles/securing-the-gateway-with-client-mtls.mdx) - Mutual TLS
|
|
80
|
+
certificate authentication
|
|
81
81
|
- [LDAP](../policies/ldap-auth-inbound.mdx) - LDAP directory authentication
|
|
82
82
|
- [HMAC](../policies/hmac-auth-inbound.mdx) - Hash-based message authentication
|
|
83
83
|
|
|
@@ -15,9 +15,13 @@ certificate is present. If a parseable certificate is present, the policy still
|
|
|
15
15
|
sets `request.user.data.mtlsAuth`; otherwise it leaves the request unchanged.
|
|
16
16
|
|
|
17
17
|
Set `certIssuerDN` to the fully qualified issuer distinguished name to require
|
|
18
|
-
on the client certificate.
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
on the client certificate. The policy rejects certificates whose parsed issuer
|
|
19
|
+
DN does not match. `certIssuerDN` is required whenever enforcement is enabled
|
|
20
|
+
(i.e. when `allowUnauthenticatedRequests` is not `true`); the policy fails to
|
|
21
|
+
load otherwise. This guarantees that requests are pinned to a specific CA and is
|
|
22
|
+
especially important when an account has multiple CAs configured.
|
|
23
|
+
|
|
24
|
+
Comparison is order-sensitive on RDNs (e.g. `"CN=foo, O=bar"` does not match
|
|
21
25
|
`"O=bar, CN=foo"`, which matches RFC 4514 §2.1 semantics) but tolerant of
|
|
22
26
|
casing and whitespace, so `"CN=example-ca, O=Example, C=US"` matches
|
|
23
27
|
`"cn=Example-CA,o=example,c=us"`. Multi-valued RDNs (`+`) and hex-encoded
|
|
@@ -25,8 +29,5 @@ values (`#...`) are not normalized. The simplest way to obtain the expected
|
|
|
25
29
|
value is to inspect `request.user.data.mtlsAuth.issuer` from a request signed
|
|
26
30
|
by the desired CA.
|
|
27
31
|
|
|
28
|
-
The `certIssuerDN` is useful when you want to distinguish between client certs from
|
|
29
|
-
different CAs if you have multiple set on your account. It is recommended to set this by default.
|
|
30
|
-
|
|
31
32
|
Note: this policy does not work with local development since it relies on metadata from the upstream reverse proxy,
|
|
32
33
|
it is recommended to test this using a working-copy or preview environment.
|
|
@@ -41,10 +41,23 @@
|
|
|
41
41
|
},
|
|
42
42
|
"certIssuerDN": {
|
|
43
43
|
"type": "string",
|
|
44
|
-
"description": "
|
|
44
|
+
"description": "Fully qualified issuer distinguished name to require on the client certificate. The policy rejects certificates whose parsed issuer DN does not match this string exactly. Required unless `allowUnauthenticatedRequests` is `true`. The expected format matches the parsed metadata issuer, e.g. \"CN=example-ca, O=Example, C=US\".",
|
|
45
45
|
"examples": ["CN=example-ca, O=Example, C=US"]
|
|
46
46
|
}
|
|
47
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"anyOf": [
|
|
49
|
+
{
|
|
50
|
+
"required": ["certIssuerDN"]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"properties": {
|
|
54
|
+
"allowUnauthenticatedRequests": {
|
|
55
|
+
"const": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"required": ["allowUnauthenticatedRequests"]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
48
61
|
}
|
|
49
62
|
},
|
|
50
63
|
"examples": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "6.70.
|
|
3
|
+
"version": "6.70.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The programmable API Gateway",
|
|
6
6
|
"author": "Zuplo, Inc.",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"zuplo": "zuplo.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zuplo/cli": "6.70.
|
|
23
|
-
"@zuplo/core": "6.70.
|
|
24
|
-
"@zuplo/runtime": "6.70.
|
|
22
|
+
"@zuplo/cli": "6.70.26",
|
|
23
|
+
"@zuplo/core": "6.70.26",
|
|
24
|
+
"@zuplo/runtime": "6.70.26",
|
|
25
25
|
"@zuplo/test": "1.4.0"
|
|
26
26
|
}
|
|
27
27
|
}
|