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
- The recommended approach is to read the route parameters in an inbound policy,
30
- transform them, store the results on
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: any,
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
- ## Common Pitfall: Modifying `request.params` Directly
146
+ ## Alternative: Modify Route Parameters Directly
142
147
 
143
- :::caution
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
- Do not try to transform route parameters by constructing a new `ZuploRequest`
146
- with modified `params` and expecting the URL Rewrite handler to pick them up.
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
- ```ts
154
- // ⚠️ This approach does NOT work as expected with URL Rewrite
155
- const newRequest = new ZuploRequest(request, {
156
- params: {
157
- ...request.params,
158
- resourceType: "customerorder",
159
- },
160
- });
161
- return newRequest;
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
- In practice, the URL Rewrite handler evaluates `${params.*}` against the
165
- route-level parameters rather than the request object returned by a policy. This
166
- means the rewritten URL may contain `undefined` segments instead of your
167
- transformed values. Use `context.custom` for reliable interpolation of
168
- transformed values — the URL Rewrite handler's `rewritePattern` fully supports
169
- `${context.custom.*}`, and values set in an inbound policy are available when
170
- the handler runs.
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: any,
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: any,
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: any,
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.13",
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.13",
23
- "@zuplo/core": "6.71.13",
24
- "@zuplo/runtime": "6.71.13",
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
  }