Skip to content

Commit f5ce463

Browse files
authored
feat: GAP authz w/ docker compose (#997)
* feat: setup-gap authz ext * fix: dns resolution fixes * docs: document goose chase * fix: sanitize variables before logging * chore: cleanup * chore: add changesets * fix: dns * fix: recursive loop for localhost requests * chore: log status code during envoy check * fix: localhost websocket listener * docs: include note about docker compose dns * chore: bump alpine image version * fix: unique name for docker network
1 parent 9a35eb4 commit f5ce463

11 files changed

Lines changed: 593 additions & 751 deletions

File tree

.changeset/gold-wolves-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"setup-gap": minor
3+
---
4+
5+
fix DNS issue for dynamic proxy

.changeset/poor-planets-deliver.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"setup-gap": major
3+
---
4+
5+
replace docker run with docker compose

.changeset/slimy-islands-swim.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"setup-gap": major
3+
---
4+
5+
replace blocking LUA script with authz filter

actions/setup-gap/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,87 @@
66

77
The contents of ./aws-ca.crt are pulled from the PEM files on the
88
[AWS Certificate Authority](https://www.amazontrust.com/repository/) page.
9+
10+
## Configuration Gotcha
11+
12+
This sets up a local envoy proxy which is solely for augmenting the request with
13+
the necessary authorization headers, before it send a request to the upstream.
14+
15+
Here is a major issue with the dynamic proxy. Example request:
16+
17+
1. `curl "https://service.our.domain.com"`
18+
2. DNS lookup on the host machine resolves this domain to `127.0.0.1`
19+
1. This is because we have made a specific entry in our `/etc/hosts` to
20+
ensure this is routed through the local proxy
21+
3. Request is routed to the local envoy proxy, for the sake of this example,
22+
lets assume it uses the dynamic proxy listener
23+
4. The dynamic proxy checks what kind of connection it is and handles, let's
24+
assume it's an http request
25+
5. Envoy does TLS termination, it's SAN ext allows it to use hostname
26+
6. Envoy augments the request before proxying it upstream, adding authentication
27+
headers using whichever method (lua or ext authz)
28+
7. Envoy takes the 'host' header from the original request, and attempts to
29+
resolve that host (or use a cached entry)
30+
1. Resolving `service.our.domain.com` may resolve to `127.0.0.1` because of
31+
DNS resolution caching, and because of `/etc/hosts` entry
32+
8. If resolved to `127.0.0.1` envoy attempts to intiate a connection with it's
33+
own listener.
34+
1. This fails because the TLS cert is not valid according to the envoy
35+
container's CAs
36+
9. If resolved to the proper upstream, everything works fine.
37+
38+
### How to diagnose this?
39+
40+
1. Intermittent failures
41+
1. If this is failing occasionally it's probably because of this. It mostly
42+
all depends on how the upstream resolves, and if the good/bad entry has
43+
been cached or not.
44+
2. TLS errors
45+
1. Curls to the endpoint will result in a 503 with logs like
46+
```
47+
curl: (22) The requested URL returned error: 503
48+
upstream connect error or disconnect/reset before headers. retried and the latest reset reason: remote connection failure, transport failure reason: TLS_error:|268435581:SSL routines:OPENSSL_internal:CERTIFICATE_VERIFY_FAILED:TLS_error_end
49+
```
50+
2. Envoy logs like:
51+
```
52+
[2025-04-15 21:45:11.169][15][debug][connection] [source/common/tls/cert_validator/default_validator.cc:339] verify cert failed: X509_verify_cert: certificate verification error at depth 0: unable to get local issuer certificate
53+
[2025-04-15 21:45:11.169][15][debug][connection] [source/common/tls/ssl_socket.cc:248] [Tags: "ConnectionId":"2"] remote address:127.0.0.1:443,TLS_error:|268435581:SSL routines:OPENSSL_internal:CERTIFICATE_VERIFY_FAILED:TLS_error_end
54+
```
55+
3. Infintely recursive connections
56+
1. If you are debugging and set `trust_chain_verification: ACCEPT_UNTRUSTED`,
57+
then the TLS errors will disappear and you will get a recursive loop of
58+
proxies, until the original request times out.
59+
60+
### Resolution
61+
62+
#### Envoy Config
63+
64+
Forcing DNS resolution for the upstream to use a hardcoded DNS server. **Note:**
65+
The cluster config and listener config need to have similar configs otherwise
66+
it's an invalid config.
67+
68+
```
69+
dns_cache_config:
70+
name: dynamic_forward_proxy_cache_config
71+
dns_lookup_family: V4_ONLY
72+
dns_resolution_config:
73+
resolvers:
74+
- socket_address:
75+
address: 8.8.8.8
76+
port_value: 53
77+
dns_resolver_options:
78+
use_tcp_for_dns_lookups: true
79+
host_ttl: 60s
80+
```
81+
82+
#### Docker Compose
83+
84+
Add specific DNS servers to the compose configuration. These may not be
85+
necessary but we believe they ensure that the host system's DNS cache is not
86+
used.
87+
88+
```
89+
dns:
90+
- 8.8.8.8
91+
- 8.8.4.4
92+
```

actions/setup-gap/action.yml

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ inputs:
117117
default: ""
118118
required: false
119119

120+
auth-service-port:
121+
description: "The port the auth service will listen on."
122+
required: false
123+
default: "9001"
124+
120125
outputs:
121126
local-proxy-port:
122127
description: "The port the local proxy will listen on."
@@ -264,8 +269,9 @@ runs:
264269
# Verify the installation by checking version
265270
gomplate --version
266271
267-
- name: Run local Envoy proxy
268-
shell: sh
272+
- name: Setup and run services
273+
id: setup-services
274+
shell: bash
269275
env:
270276
GAP_NAME: "gap-${{ inputs.gap-name }}"
271277
DYNAMIC_PROXY_PORT: ${{ inputs.dynamic-proxy-port }}
@@ -279,6 +285,15 @@ runs:
279285
PROXY_PORT: ${{ inputs.proxy-port }}
280286
WEBSOCKETS_PROXY_PORT: ${{ inputs.websockets-proxy-port }}
281287
WEBSOCKETS_SERVICES: ${{ inputs.websockets-services }}
288+
AUTH_SERVICE_NAME: ${{ inputs.gap-name }}-authz
289+
AUTH_SERVICE_PORT: ${{ inputs.auth-service-port }}
290+
PATH_CERTS_DIR: ${{ env.PATH_CERTS_DIR }}
291+
REQUIRED_ENV_VARS: >-
292+
WEBSOCKETS_PROXY_PORT DYNAMIC_PROXY_PORT PROXY_PORT
293+
K8S_API_ENDPOINT_PORT MAIN_DNS_ZONE ENVOY_PROXY_IMAGE
294+
ENABLE_PROXY_DEBUG PROXY_LOG_LEVEL AUTH_SERVICE_NAME AUTH_SERVICE_PORT
295+
ACTIONS_ID_TOKEN_REQUEST_TOKEN ACTIONS_ID_TOKEN_REQUEST_URL
296+
GITHUB_REPOSITORY GITHUB_OIDC_TOKEN_HEADER_NAME GITHUB_OIDC_HOSTNAME
282297
run: |
283298
# Get the Github OIDC hostname
284299
export GITHUB_OIDC_HOSTNAME=$(echo $ACTIONS_ID_TOKEN_REQUEST_URL | awk -F[/:] '{print $4}')
@@ -294,11 +309,8 @@ runs:
294309
295310
echo "Using log level: ${PROXY_LOG_LEVEL}"
296311
297-
# List of required ENVs to check
298-
required_env_vars="DYNAMIC_PROXY_PORT ENABLE_PROXY_DEBUG GITHUB_OIDC_TOKEN_HEADER_NAME ENVOY_PROXY_IMAGE GITHUB_OIDC_HOSTNAME K8S_API_ENDPOINT_PORT MAIN_DNS_ZONE PROXY_LOG_LEVEL PROXY_PORT GITHUB_REPOSITORY WEBSOCKETS_PROXY_PORT"
299-
300312
# Loop through each variable and check if it's empty
301-
for var in $required_env_vars; do
313+
for var in $REQUIRED_ENV_VARS; do
302314
eval value=\$$var
303315
if [ -z "$value" ]; then
304316
echo "::error::Required environment variable '$var' is not set."
@@ -309,45 +321,30 @@ runs:
309321
# Generate Envoy config from template
310322
gomplate -f "${GITHUB_ACTION_PATH}/envoy.yaml.gotmpl" -o "${GITHUB_ACTION_PATH}/envoy.yaml"
311323
312-
# Generate LUA script from template
313-
gomplate -f "${GITHUB_ACTION_PATH}/github_oidc.lua.gotmpl" -o "${GITHUB_ACTION_PATH}/github_oidc.lua"
314-
315324
# Copy AWS CA certs
316325
cp ${{ github.action_path }}/aws-ca.crt "${PATH_CERTS_DIR}"
317326
chmod 644 "${PATH_CERTS_DIR}/server.key"
318327
319328
echo "Validating Envoy config..."
320329
if ! docker run --rm \
321-
--name "${GAP_NAME}" \
322-
-p "${DYNAMIC_PROXY_PORT}:${DYNAMIC_PROXY_PORT}" \
323-
-p "${PROXY_PORT}:${PROXY_PORT}" \
324-
-p "${WEBSOCKETS_PROXY_PORT}:${WEBSOCKETS_PROXY_PORT}" \
325-
-v "${PATH_CERTS_DIR}":/tls \
326-
-v "${GITHUB_ACTION_PATH}/envoy.yaml":/etc/envoy/envoy.yaml \
327-
-v "${GITHUB_ACTION_PATH}/json.lua":/etc/envoy/json.lua \
328-
-v "${GITHUB_ACTION_PATH}/github_oidc.lua":/etc/envoy/github_oidc.lua \
330+
--dns 8.8.8.8
331+
--dns 8.8.4.4
332+
--volume "${PATH_CERTS_DIR}":/tls \
333+
--volume "${GITHUB_ACTION_PATH}/envoy.yaml":/etc/envoy/envoy.yaml \
329334
"${ENVOY_PROXY_IMAGE}" \
330335
/usr/local/bin/envoy --mode validate -c /etc/envoy/envoy.yaml \
331-
--log-level "${PROXY_LOG_LEVEL}" \
332-
"${ENVOY_EXTRA_ARGS}"; then
336+
--log-level "${PROXY_LOG_LEVEL}" "${ENVOY_EXTRA_ARGS}"; then
333337
echo "::error::Envoy configuration validation failed."
334338
exit 1
335339
fi
336340
337-
echo "Starting the local Envoy proxy..."
338-
docker run --rm -d \
339-
--name "${GAP_NAME}" \
340-
-p "${DYNAMIC_PROXY_PORT}:${DYNAMIC_PROXY_PORT}" \
341-
-p "${PROXY_PORT}:${PROXY_PORT}" \
342-
-p "${WEBSOCKETS_PROXY_PORT}:${WEBSOCKETS_PROXY_PORT}" \
343-
-v "${PATH_CERTS_DIR}":/tls \
344-
-v "${GITHUB_ACTION_PATH}/envoy.yaml":/etc/envoy/envoy.yaml \
345-
-v "${GITHUB_ACTION_PATH}/json.lua":/etc/envoy/json.lua \
346-
-v "${GITHUB_ACTION_PATH}/github_oidc.lua":/etc/envoy/github_oidc.lua \
347-
"${ENVOY_PROXY_IMAGE}" \
348-
/usr/local/bin/envoy -c /etc/envoy/envoy.yaml \
349-
--log-level "${PROXY_LOG_LEVEL}" \
350-
"${ENVOY_EXTRA_ARGS}"
341+
if [ "$ENABLE_PROXY_DEBUG" = "true" ] || [ "$PROXY_LOG_LEVEL" = "debug" ]; then
342+
echo "Docker compose configuration:"
343+
docker compose -f "${GITHUB_ACTION_PATH}/docker-compose.yml" config
344+
fi
345+
346+
echo "Starting the auth service and Envoy proxy..."
347+
docker compose -f "${GITHUB_ACTION_PATH}/docker-compose.yml" up -d
351348
352349
- name: Verify Envoy Proxy
353350
shell: bash
@@ -361,11 +358,24 @@ runs:
361358
local port=$1
362359
local name=$2
363360
364-
echo "Checking if the ${name} proxy is up and running on [https://localhost:${port}] ..."
361+
echo "Checking if the ${name} proxy is up and running on [https://localhost:${port}]"
365362
for attempt in {1..10}; do
366-
if curl --silent --cacert "${PATH_CERTS_DIR}/ca.crt" --head https://localhost:${port} > /dev/null 2>&1; then
367-
echo "${name} proxy is up, and the HTTPS connection is successful."
363+
# Get the status code using curl
364+
status_code=$(curl --silent --cacert "${PATH_CERTS_DIR}/ca.crt" \
365+
-o /dev/null -w "%{http_code}" https://localhost:${port})
366+
367+
echo "Attempt ${attempt}/10: Status code: ${status_code}"
368+
369+
if [[ $status_code -eq 200 ]]; then
370+
echo "${name} proxy is up, and the HTTPS connection is successful with status code 200."
368371
return 0
372+
elif [[ $status_code -ne 000 ]]; then
373+
echo "WARNING: ${name} proxy returned non-200 status code: ${status_code}"
374+
if [[ $attempt -eq 10 ]]; then
375+
echo "::error:: ${name} proxy is responding but with unexpected status code: ${status_code}"
376+
return 1
377+
fi
378+
sleep 3
369379
else
370380
echo "Waiting for the ${name} proxy to start... Attempt ${attempt}/10"
371381
sleep 3

actions/setup-gap/authz/Dockerfile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM golang:1.24-alpine AS builder
2+
3+
WORKDIR /app
4+
COPY main.go .
5+
RUN go mod init github.com/auth-service && \
6+
go build -o auth-service .
7+
8+
FROM alpine:3.21
9+
10+
RUN apk --no-cache add ca-certificates
11+
WORKDIR /app
12+
COPY --from=builder /app/auth-service .
13+
14+
ENV AUTH_SERVICE_PORT=9001
15+
EXPOSE ${AUTH_SERVICE_PORT}
16+
17+
CMD ["/app/auth-service"]

0 commit comments

Comments
 (0)