zuplo 6.71.13 → 6.71.15
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.
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Connect to an AWS ALB with mTLS
|
|
3
|
+
sidebar_label: Connect to an AWS ALB with mTLS
|
|
4
|
+
description:
|
|
5
|
+
Configure Zuplo to authenticate to an AWS Application Load Balancer using a
|
|
6
|
+
mutual TLS client certificate, so the ALB only accepts traffic that comes
|
|
7
|
+
through your gateway.
|
|
8
|
+
tags:
|
|
9
|
+
- backends
|
|
10
|
+
- authentication
|
|
11
|
+
- deployment
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<EnterpriseFeature name="mTLS Client Certificates" />
|
|
15
|
+
|
|
16
|
+
When your backend sits behind an AWS Application Load Balancer (ALB), you can
|
|
17
|
+
lock the ALB down so it only accepts requests that prove they came from your
|
|
18
|
+
Zuplo gateway. ALB mutual TLS (mTLS) in **verify mode** requires every client to
|
|
19
|
+
present an X.509 certificate that chains to a Certificate Authority (CA) in the
|
|
20
|
+
ALB's trust store. Zuplo presents that client certificate on each outbound
|
|
21
|
+
request, and the ALB rejects anything that can't.
|
|
22
|
+
|
|
23
|
+
This guide covers the Zuplo side of that connection: uploading a client
|
|
24
|
+
certificate and presenting it on requests to the ALB. It does **not** cover
|
|
25
|
+
configuring the ALB itself — for that, follow the AWS documentation linked in
|
|
26
|
+
[Configure the ALB](#1-configure-the-alb).
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
The ALB's HTTPS listener is configured for mTLS verify mode and backed by a
|
|
31
|
+
trust store that contains the CA which issued Zuplo's client certificate. On
|
|
32
|
+
each request, the gateway presents its client certificate, the ALB verifies it
|
|
33
|
+
against the trust store, and only then forwards the request to the target group.
|
|
34
|
+
|
|
35
|
+
<Diagram height="h-64">
|
|
36
|
+
<DiagramNode id="client">Client</DiagramNode>
|
|
37
|
+
<DiagramNode id="gateway" variant="zuplo">
|
|
38
|
+
Zuplo Gateway
|
|
39
|
+
</DiagramNode>
|
|
40
|
+
<DiagramGroup id="aws" label="AWS VPC">
|
|
41
|
+
<DiagramNode id="alb" variant="orange">
|
|
42
|
+
ALB (mTLS verify)
|
|
43
|
+
</DiagramNode>
|
|
44
|
+
<DiagramNode id="backend" variant="green">
|
|
45
|
+
Backend targets
|
|
46
|
+
</DiagramNode>
|
|
47
|
+
</DiagramGroup>
|
|
48
|
+
<DiagramEdge from="client" to="gateway" label="HTTPS" />
|
|
49
|
+
<DiagramEdge from="gateway" to="alb" label="mTLS (client cert)" />
|
|
50
|
+
<DiagramEdge from="alb" to="backend" label="Forwarded" />
|
|
51
|
+
</Diagram>
|
|
52
|
+
|
|
53
|
+
This gives you two guarantees at once:
|
|
54
|
+
|
|
55
|
+
- **The ALB trusts the gateway.** The load balancer rejects requests that don't
|
|
56
|
+
present a valid client certificate before they reach your application.
|
|
57
|
+
- **The gateway trusts the ALB.** Standard TLS still verifies the ALB's server
|
|
58
|
+
certificate, so the gateway knows it's talking to the real backend.
|
|
59
|
+
|
|
60
|
+
For background on the gateway-to-origin direction in general, see
|
|
61
|
+
[Gateway to Origin mTLS Authentication](./securing-backend-mtls.mdx).
|
|
62
|
+
|
|
63
|
+
## Prerequisites
|
|
64
|
+
|
|
65
|
+
Before you begin, you need:
|
|
66
|
+
|
|
67
|
+
- A client certificate and private key (PEM-encoded) issued by a CA. The same CA
|
|
68
|
+
must be uploaded to the ALB's trust store.
|
|
69
|
+
- An AWS Application Load Balancer with an HTTPS listener you can configure for
|
|
70
|
+
mTLS.
|
|
71
|
+
- The [Zuplo CLI](../cli/overview.mdx) installed and authenticated.
|
|
72
|
+
|
|
73
|
+
## 1/ Configure the ALB
|
|
74
|
+
|
|
75
|
+
On the AWS side, configure the ALB's HTTPS listener to use mutual TLS in
|
|
76
|
+
**verify mode** and create a trust store that contains the CA which signed your
|
|
77
|
+
client certificate. Verify mode is what makes the ALB perform X.509 client
|
|
78
|
+
certificate authentication during the TLS handshake.
|
|
79
|
+
|
|
80
|
+
Follow the AWS documentation:
|
|
81
|
+
|
|
82
|
+
- [Mutual authentication with TLS in Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/mutual-authentication.html)
|
|
83
|
+
— concepts, verify vs. passthrough mode, and trust stores.
|
|
84
|
+
- [Configuring mutual TLS on an Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/configuring-mtls-with-elb.html)
|
|
85
|
+
— step-by-step listener and trust store setup.
|
|
86
|
+
|
|
87
|
+
:::caution
|
|
88
|
+
|
|
89
|
+
Use **verify** mode, not passthrough. In passthrough mode the ALB forwards the
|
|
90
|
+
client certificate to your targets without checking it, so the load balancer
|
|
91
|
+
does not enforce trust. Verify mode is what rejects untrusted callers at the
|
|
92
|
+
edge.
|
|
93
|
+
|
|
94
|
+
:::
|
|
95
|
+
|
|
96
|
+
## 2/ Upload your client certificate to Zuplo
|
|
97
|
+
|
|
98
|
+
Use the Zuplo CLI to upload the client certificate and private key to your
|
|
99
|
+
project. The CA that issued this certificate must already be in the ALB's trust
|
|
100
|
+
store.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
zuplo mtls-certificate create \
|
|
104
|
+
--cert client-cert.pem \
|
|
105
|
+
--key client-key.pem \
|
|
106
|
+
--name aws-alb-cert \
|
|
107
|
+
--account your-account \
|
|
108
|
+
--project your-project \
|
|
109
|
+
--environment-type development \
|
|
110
|
+
--environment-type preview \
|
|
111
|
+
--environment-type production
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
:::note
|
|
115
|
+
|
|
116
|
+
The certificate name must follow JavaScript's variable naming constraints
|
|
117
|
+
because you reference it by name in your configuration. The CLI validates this
|
|
118
|
+
when you create the certificate.
|
|
119
|
+
|
|
120
|
+
:::
|
|
121
|
+
|
|
122
|
+
## 3/ Present the certificate on requests to the ALB
|
|
123
|
+
|
|
124
|
+
Reference the uploaded certificate by name when the gateway forwards requests to
|
|
125
|
+
the ALB. Use the ALB's DNS name (or a custom domain pointed at it) as the
|
|
126
|
+
backend URL.
|
|
127
|
+
|
|
128
|
+
The simplest option is the [URL Forward Handler](../handlers/url-forward.mdx),
|
|
129
|
+
configured directly on a route in `config/routes.oas.json`:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"handler": {
|
|
134
|
+
"export": "urlForwardHandler",
|
|
135
|
+
"module": "$import(@zuplo/runtime)",
|
|
136
|
+
"options": {
|
|
137
|
+
"baseUrl": "https://my-alb-1234567890.us-east-1.elb.amazonaws.com",
|
|
138
|
+
"mtlsCertificate": "aws-alb-cert"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If you need to inspect or transform the request before forwarding, use a
|
|
145
|
+
[Function Handler](../handlers/custom-handler.mdx) and pass the certificate name
|
|
146
|
+
in the `zuplo` options of `fetch`:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
150
|
+
|
|
151
|
+
export default async function (request: ZuploRequest, context: ZuploContext) {
|
|
152
|
+
const response = await fetch(
|
|
153
|
+
"https://my-alb-1234567890.us-east-1.elb.amazonaws.com/api",
|
|
154
|
+
{
|
|
155
|
+
zuplo: {
|
|
156
|
+
mtlsCertificate: "aws-alb-cert",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return response;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## 4/ Use environment variables across environments
|
|
166
|
+
|
|
167
|
+
To use a different certificate per environment, store the certificate name in an
|
|
168
|
+
[environment variable](./environment-variables.mdx) and reference it with the
|
|
169
|
+
`$env()` selector:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"handler": {
|
|
174
|
+
"export": "urlForwardHandler",
|
|
175
|
+
"module": "$import(@zuplo/runtime)",
|
|
176
|
+
"options": {
|
|
177
|
+
"baseUrl": "${env.ALB_BACKEND_URL}",
|
|
178
|
+
"mtlsCertificate": "$env(ALB_MTLS_CERT)"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
In a Function Handler, read the same variable from the `environment` object:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";
|
|
188
|
+
|
|
189
|
+
export default async function (request: ZuploRequest, context: ZuploContext) {
|
|
190
|
+
const response = await fetch(`${environment.ALB_BACKEND_URL}/api`, {
|
|
191
|
+
zuplo: {
|
|
192
|
+
mtlsCertificate: environment.ALB_MTLS_CERT,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return response;
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Verify the connection
|
|
201
|
+
|
|
202
|
+
Deploy your changes to a preview or production environment, then send a request
|
|
203
|
+
through the gateway to a route that forwards to the ALB. A successful response
|
|
204
|
+
confirms the ALB accepted the client certificate.
|
|
205
|
+
|
|
206
|
+
:::warning
|
|
207
|
+
|
|
208
|
+
mTLS bindings aren't available in local development. Code that references an
|
|
209
|
+
mTLS certificate only works once deployed to a Zuplo edge environment — test in
|
|
210
|
+
a preview environment rather than locally.
|
|
211
|
+
|
|
212
|
+
:::
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
### Requests fail with a 522 or connection error
|
|
217
|
+
|
|
218
|
+
A `522` means the connection to the ALB failed before an HTTP response was
|
|
219
|
+
received — usually a TLS handshake problem. Confirm that:
|
|
220
|
+
|
|
221
|
+
- The CA that issued Zuplo's client certificate is present in the ALB's trust
|
|
222
|
+
store.
|
|
223
|
+
- The client certificate hasn't expired.
|
|
224
|
+
- The `mtlsCertificate` name in your configuration matches the name you used in
|
|
225
|
+
`zuplo mtls-certificate create`.
|
|
226
|
+
|
|
227
|
+
### The ALB rejects the certificate
|
|
228
|
+
|
|
229
|
+
If the handshake completes but the ALB returns a `403`, the certificate is being
|
|
230
|
+
presented but not trusted. Re-check the ALB trust store and confirm the listener
|
|
231
|
+
is in **verify** mode, not passthrough. If your client certificate is issued by
|
|
232
|
+
an intermediate CA, make sure the trust store contains the full chain back to
|
|
233
|
+
the root.
|
|
234
|
+
|
|
235
|
+
### No certificate appears to be sent
|
|
236
|
+
|
|
237
|
+
Confirm the upload succeeded and the certificate is enabled for the environment
|
|
238
|
+
you deployed to:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
zuplo mtls-certificate list \
|
|
242
|
+
--account your-account \
|
|
243
|
+
--project your-project
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Related resources
|
|
247
|
+
|
|
248
|
+
- [Gateway to Origin mTLS Authentication](./securing-backend-mtls.mdx)
|
|
249
|
+
- [Securing your backend](./securing-your-backend.mdx)
|
|
250
|
+
- [URL Forward Handler](../handlers/url-forward.mdx)
|
|
251
|
+
- [Function Handler](../handlers/custom-handler.mdx)
|
|
252
|
+
- [Environment Variables](./environment-variables.mdx)
|
|
253
|
+
- [Mutual authentication with TLS in Application Load Balancer (AWS)](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/mutual-authentication.html)
|
|
@@ -26,10 +26,14 @@ the rewrite happens. Common scenarios include:
|
|
|
26
26
|
- **Case normalization** — converting `Products` to `products` before forwarding
|
|
27
27
|
- **Path translation** — mapping user-friendly slugs to internal identifiers
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
One approach is to read the route parameters in an inbound policy, transform
|
|
30
|
+
them, store the results on
|
|
31
31
|
[`context.custom`](../programmable-api/zuplo-context.mdx), and reference the
|
|
32
|
-
transformed values in the URL Rewrite pattern.
|
|
32
|
+
transformed values in the URL Rewrite pattern. This keeps the original route
|
|
33
|
+
parameters intact while exposing the transformed values under clearly named
|
|
34
|
+
keys. If you prefer, you can instead modify `params` directly on the request —
|
|
35
|
+
see
|
|
36
|
+
[Alternative: Modify Route Parameters Directly](#alternative-modify-route-parameters-directly).
|
|
33
37
|
|
|
34
38
|
## Step-by-Step Example
|
|
35
39
|
|
|
@@ -54,7 +58,7 @@ import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
|
54
58
|
export default async function (
|
|
55
59
|
request: ZuploRequest,
|
|
56
60
|
context: ZuploContext,
|
|
57
|
-
options:
|
|
61
|
+
options: Record<string, unknown>,
|
|
58
62
|
policyName: string,
|
|
59
63
|
): Promise<ZuploRequest | Response> {
|
|
60
64
|
// Read the original route parameter
|
|
@@ -88,7 +92,8 @@ Add the policy to `config/policies.json`:
|
|
|
88
92
|
"policyType": "custom-code-inbound",
|
|
89
93
|
"handler": {
|
|
90
94
|
"export": "default",
|
|
91
|
-
"module": "$import(./modules/transform-params)"
|
|
95
|
+
"module": "$import(./modules/transform-params)",
|
|
96
|
+
"options": {}
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
]
|
|
@@ -138,36 +143,40 @@ pipeline as follows:
|
|
|
138
143
|
3. The URL Rewrite handler builds the upstream URL:
|
|
139
144
|
`https://backend.example.com/api/customerorder/123`
|
|
140
145
|
|
|
141
|
-
##
|
|
146
|
+
## Alternative: Modify Route Parameters Directly
|
|
142
147
|
|
|
143
|
-
|
|
148
|
+
Instead of storing transformed values on `context.custom`, you can return a new
|
|
149
|
+
[`ZuploRequest`](../programmable-api/zuplo-request.mdx) with modified `params`.
|
|
150
|
+
The URL Rewrite handler reads `${params.*}` from the request it receives, so any
|
|
151
|
+
parameters you set on the returned request flow through to the `rewritePattern`:
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
:::
|
|
149
|
-
|
|
150
|
-
A common first attempt is to create a new
|
|
151
|
-
[`ZuploRequest`](../programmable-api/zuplo-request.mdx) with different `params`:
|
|
153
|
+
```ts title="modules/transform-params-direct.ts"
|
|
154
|
+
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
152
155
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
export default async function (
|
|
157
|
+
request: ZuploRequest,
|
|
158
|
+
context: ZuploContext,
|
|
159
|
+
options: Record<string, unknown>,
|
|
160
|
+
policyName: string,
|
|
161
|
+
): Promise<ZuploRequest | Response> {
|
|
162
|
+
return new ZuploRequest(request, {
|
|
163
|
+
params: {
|
|
164
|
+
...request.params,
|
|
165
|
+
resourceType: `customer${request.params.resourceType}`,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
162
169
|
```
|
|
163
170
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
the
|
|
171
|
+
With this policy in front of a URL Rewrite handler whose `rewritePattern` is
|
|
172
|
+
`https://backend.example.com/api/${params.resourceType}/${params.resourceId}`,
|
|
173
|
+
the handler forwards a request for `/api/order/123` to
|
|
174
|
+
`https://backend.example.com/api/customerorder/123`.
|
|
175
|
+
|
|
176
|
+
Both approaches work. Modify `params` directly when you simply want the
|
|
177
|
+
rewritten URL to use the transformed values. Use `context.custom` when you want
|
|
178
|
+
to keep the original route parameters available (for logging or for use
|
|
179
|
+
elsewhere in the pipeline) while passing transformed values alongside them.
|
|
171
180
|
|
|
172
181
|
## Variations
|
|
173
182
|
|
|
@@ -190,7 +199,7 @@ const RESOURCE_TYPE_MAP: Record<string, string> = {
|
|
|
190
199
|
export default async function (
|
|
191
200
|
request: ZuploRequest,
|
|
192
201
|
context: ZuploContext,
|
|
193
|
-
options:
|
|
202
|
+
options: Record<string, unknown>,
|
|
194
203
|
policyName: string,
|
|
195
204
|
): Promise<ZuploRequest | Response> {
|
|
196
205
|
const resourceType = request.params.resourceType;
|
|
@@ -219,7 +228,7 @@ import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
|
219
228
|
export default async function (
|
|
220
229
|
request: ZuploRequest,
|
|
221
230
|
context: ZuploContext,
|
|
222
|
-
options:
|
|
231
|
+
options: Record<string, unknown>,
|
|
223
232
|
policyName: string,
|
|
224
233
|
): Promise<ZuploRequest | Response> {
|
|
225
234
|
// Normalize casing
|
|
@@ -254,7 +263,7 @@ import { ZuploContext, ZuploRequest } from "@zuplo/runtime";
|
|
|
254
263
|
export default async function (
|
|
255
264
|
request: ZuploRequest,
|
|
256
265
|
context: ZuploContext,
|
|
257
|
-
options:
|
|
266
|
+
options: Record<string, unknown>,
|
|
258
267
|
policyName: string,
|
|
259
268
|
): Promise<ZuploRequest | Response> {
|
|
260
269
|
// Transform route parameter
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "6.71.
|
|
3
|
+
"version": "6.71.15",
|
|
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.71.
|
|
23
|
-
"@zuplo/core": "6.71.
|
|
24
|
-
"@zuplo/runtime": "6.71.
|
|
22
|
+
"@zuplo/cli": "6.71.15",
|
|
23
|
+
"@zuplo/core": "6.71.15",
|
|
24
|
+
"@zuplo/runtime": "6.71.15",
|
|
25
25
|
"@zuplo/test": "1.4.0"
|
|
26
26
|
}
|
|
27
27
|
}
|