diff --git a/.changeset/shaggy-wolves-unite.md b/.changeset/shaggy-wolves-unite.md new file mode 100644 index 00000000..b2895d10 --- /dev/null +++ b/.changeset/shaggy-wolves-unite.md @@ -0,0 +1,9 @@ +--- +"setup-gap": minor +--- + +- fix: (action) add --project parameter for docker compose commands +- fix: (envoy config) websocket service routing with explicit port +- fix: (authz + envoy config) host header port rewrites +- fix: (authz) logging level environment variable name +- feat: (authz) better header debug logging diff --git a/actions/setup-gap/action.yml b/actions/setup-gap/action.yml index 5537f39a..134b0704 100644 --- a/actions/setup-gap/action.yml +++ b/actions/setup-gap/action.yml @@ -321,14 +321,15 @@ runs: PROXY_PORT: ${{ inputs.proxy-port }} WEBSOCKETS_PROXY_PORT: ${{ inputs.websockets-proxy-port }} WEBSOCKETS_SERVICES: ${{ inputs.websockets-services }} + ENVOY_SERVICE_NAME: ${{ inputs.gap-name }}-envoy AUTH_SERVICE_NAME: ${{ inputs.gap-name }}-authz AUTH_SERVICE_PORT: ${{ inputs.auth-service-port }} PATH_CERTS_DIR: ${{ env.PATH_CERTS_DIR }} DEBUG_MODE: ${{ steps.enable-debug.outputs.debug-mode }} REQUIRED_ENV_VARS: >- - WEBSOCKETS_PROXY_PORT DYNAMIC_PROXY_PORT PROXY_PORT + GAP_NAME WEBSOCKETS_PROXY_PORT DYNAMIC_PROXY_PORT PROXY_PORT K8S_API_ENDPOINT_PORT MAIN_DNS_ZONE ENVOY_PROXY_IMAGE PROXY_LOG_LEVEL - AUTH_LOG_LEVEL AUTH_SERVICE_NAME AUTH_SERVICE_PORT + AUTH_LOG_LEVEL ENVOY_SERVICE_NAME AUTH_SERVICE_NAME AUTH_SERVICE_PORT ACTIONS_ID_TOKEN_REQUEST_TOKEN ACTIONS_ID_TOKEN_REQUEST_URL GITHUB_REPOSITORY GITHUB_OIDC_TOKEN_HEADER_NAME GITHUB_OIDC_HOSTNAME run: | @@ -374,11 +375,11 @@ runs: if [[ "$DEBUG_MODE" == "true" ]]; then echo "Docker compose configuration:" - docker compose -f "${GITHUB_ACTION_PATH}/docker-compose.yml" config + docker compose -p ${GAP_NAME} -f "${GITHUB_ACTION_PATH}/docker-compose.yml" config fi echo "Starting the auth service and Envoy proxy..." - docker compose -f "${GITHUB_ACTION_PATH}/docker-compose.yml" up -d + docker compose -p ${GAP_NAME} -f "${GITHUB_ACTION_PATH}/docker-compose.yml" up -d - name: Verify Envoy Proxy shell: bash diff --git a/actions/setup-gap/authz/main.go b/actions/setup-gap/authz/main.go index 25d85f45..311aade6 100644 --- a/actions/setup-gap/authz/main.go +++ b/actions/setup-gap/authz/main.go @@ -58,6 +58,7 @@ type OIDCResponse struct { Value string `json:"value"` } +// https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-okhttpresponse type AuthResponse struct { Status struct { Code int `json:"code"` @@ -165,6 +166,7 @@ func fetchGitHubOIDCToken() (string, int64, error) { func ensurePort443(authority string) string { // If MainDNSZone is not in the authority, no need to process if !strings.Contains(authority, config.MainDNSZone) { + logDebug("Authority does not contain MainDNSZone (%s), no changes made: %s", config.MainDNSZone, sanitizeStr(authority)) return authority } @@ -184,20 +186,33 @@ func sanitizeStr(value string) string { return sanitizedValue } -func addHeader(w http.ResponseWriter, headerName, headerValue string, logValue bool) { +func addHeader(w http.ResponseWriter, authResp AuthResponse, headerName, headerValue string, logValue bool) { if logValue { - logDebug("Adding header: %s=%s", headerName, sanitizeStr(headerValue)) + logDebug(" Adding header: %s=%s", headerName, sanitizeStr(headerValue)) } else { - logDebug("Adding header: %s", headerName) + logDebug(" Adding header: %s", headerName) } + // Set both the header in the HTTP response and the HTTP Body w.Header().Set(headerName, headerValue) + authResp.HttpResponse.Headers[headerName] = headerValue } // handleCheck processes auth requests from Envoy func handleCheck(w http.ResponseWriter, r *http.Request) { logDefault("Check: %s %s %s", r.Method, sanitizeStr(r.URL.Path), sanitizeStr(r.UserAgent())) + for name, values := range r.Header { + for _, value := range values { + logDebug(" Header: %s=%s", name, sanitizeStr(value)) + } + } + logDebug(" Header: :method=%s", r.Method) + logDebug(" Header: :path=%s", sanitizeStr(r.URL.Path)) + if host := r.Host; host != "" { + logDebug(" Header: host=%s", sanitizeStr(host)) + } + // Fetch or refresh the token token, _, err := fetchGitHubOIDCToken() if err != nil { @@ -207,31 +222,37 @@ func handleCheck(w http.ResponseWriter, r *http.Request) { return } - // Prepare the response + // Get the authority header - this is typically the host header, but we check :authority first + authority := r.Header.Get(":authority") + if authority == "" { + authority = r.Host + logDefault("No :authority header found, using host header (%s)", sanitizeStr(authority)) + } + + // Always modify the authority to ensure port 443 + if authority != "" { + authority = ensurePort443(authority) + logDefault("Setting upstream authority to: %s", sanitizeStr(authority)) + } else { + logDefault("Warning: No authority found in request") + } + authResp := AuthResponse{} authResp.Status.Code = 200 authResp.HttpResponse.Headers = make(map[string]string) - // Add the JWT token header - headerName := config.GithubOidcTokenHeaderName - addHeader(w, headerName, "Bearer "+token, false) - - githubRepository := config.GithubRepository - addHeader(w, "x-repository", githubRepository, true) + addHeader(w, authResp, config.GithubOidcTokenHeaderName, "Bearer "+token, false) + addHeader(w, authResp, "x-repository", config.GithubRepository, true) - // Check and modify the authority header if needed - authority := r.Header.Get(":authority") if authority != "" { - modifiedAuthority := ensurePort443(authority) - addHeader(w, ":authority", modifiedAuthority, true) + addHeader(w, authResp, ":authority", authority, true) + addHeader(w, authResp, "host", authority, true) } - // Send the response w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(authResp) } -// handleHealthz is a simple health check endpoint func handleHealthz(w http.ResponseWriter, r *http.Request) { logDebug("Health check request: %s", sanitizeStr(r.URL.Path)) fmt.Fprint(w, "OK") @@ -243,8 +264,6 @@ func handleNotFound(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not Found", http.StatusNotFound) } -// loadConfig loads all environment variables into the config struct -// Returns an error if any required environment variables are missing func loadConfig() error { var missingVars []string @@ -283,7 +302,7 @@ func loadConfig() error { missingVars = append(missingVars, "AUTH_SERVICE_PORT") } - // Set log level from environment variable, default to LogLevelInfo + // Set log level, default to LogLevelDefault logLevelStr := os.Getenv("LOG_LEVEL") if logLevelStr != "" { switch strings.ToLower(logLevelStr) { @@ -318,17 +337,13 @@ func main() { logDefault("Initial token fetch failed: %v", err) } - // Set up HTTP server with custom mux mux := http.NewServeMux() - // Register /check and /check/* endpoints mux.HandleFunc("/check/", handleCheck) mux.HandleFunc("/check", handleCheck) mux.HandleFunc("/healthz", handleHealthz) - - // Set up a catch-all handler for any other paths - mux.HandleFunc("/", handleNotFound) + mux.HandleFunc("/", handleNotFound) // Catch-all for all other paths // Listen on all interfaces - required when containerized address := fmt.Sprintf("0.0.0.0:%s", config.AuthServicePort) diff --git a/actions/setup-gap/docker-compose.yml b/actions/setup-gap/docker-compose.yml index 405ed1d6..c62ce931 100644 --- a/actions/setup-gap/docker-compose.yml +++ b/actions/setup-gap/docker-compose.yml @@ -13,7 +13,7 @@ services: - ACTIONS_ID_TOKEN_REQUEST_URL=${ACTIONS_ID_TOKEN_REQUEST_URL} - ACTIONS_ID_TOKEN_REQUEST_TOKEN=${ACTIONS_ID_TOKEN_REQUEST_TOKEN} - GITHUB_REPOSITORY=${GITHUB_REPOSITORY} - - AUTH_LOG_LEVEL=${AUTH_LOG_LEVEL} + - LOG_LEVEL=${AUTH_LOG_LEVEL} healthcheck: test: [ @@ -31,7 +31,7 @@ services: envoy: image: ${ENVOY_PROXY_IMAGE} - container_name: ${GAP_NAME} + container_name: ${ENVOY_SERVICE_NAME} dns: - 8.8.8.8 - 8.8.4.4 diff --git a/actions/setup-gap/envoy.yaml.gotmpl b/actions/setup-gap/envoy.yaml.gotmpl index 1807fba7..65056ef5 100644 --- a/actions/setup-gap/envoy.yaml.gotmpl +++ b/actions/setup-gap/envoy.yaml.gotmpl @@ -129,7 +129,7 @@ static_resources: route_config: virtual_hosts: - name: dynamic_forward_host - domains: ["*.{{ getenv "MAIN_DNS_ZONE" }}"] + domains: ["*.{{ getenv "MAIN_DNS_ZONE" }}", "*.{{ getenv "MAIN_DNS_ZONE" }}:{{ getenv "DYNAMIC_PROXY_PORT" }}"] routes: - match: prefix: / @@ -170,9 +170,7 @@ static_resources: - exact: "x-repository" - exact: "{{ getenv "GITHUB_OIDC_TOKEN_HEADER_NAME" }}" - exact: ":authority" - allowed_headers: - patterns: - - exact: ":authority" + - exact: "host" failure_mode_allow: false - name: envoy.filters.http.dynamic_forward_proxy typed_config: @@ -255,7 +253,7 @@ static_resources: name: local_route virtual_hosts: - name: gap_ws_echo - domains: ["gap-ws-echo.{{ getenv "MAIN_DNS_ZONE" }}*"] + domains: ["gap-ws-echo.{{ getenv "MAIN_DNS_ZONE" }}", "gap-ws-echo.{{ getenv "MAIN_DNS_ZONE" }}:{{ getenv "WEBSOCKETS_PROXY_PORT" }}"] routes: - match: prefix: "/" @@ -281,7 +279,7 @@ static_resources: {{- $services := getenv "WEBSOCKETS_SERVICES" | strings.Split "," }} {{- range $services }} - name: {{ replaceAll "-" "_" . }} - domains: ["{{ . }}.{{ getenv "MAIN_DNS_ZONE" }}*"] + domains: ["{{ . }}.{{ getenv "MAIN_DNS_ZONE" }}", "{{ . }}.{{ getenv "MAIN_DNS_ZONE" }}:{{ getenv "WEBSOCKETS_PROXY_PORT" }}"] routes: - match: prefix: "/" diff --git a/actions/setup-gap/scripts/debug-docker-compose.sh b/actions/setup-gap/scripts/debug-docker-compose.sh new file mode 100755 index 00000000..73911fe0 --- /dev/null +++ b/actions/setup-gap/scripts/debug-docker-compose.sh @@ -0,0 +1,27 @@ +#! /bin/bash +export GAP_NAME="gap-name" +export GITHUB_ACTION_PATH="." + +export AUTH_SERVICE_NAME="auth-service-name" +export AUTH_SERVICE_PORT="9001" +export AUTH_LOG_LEVEL="debug" + +export MAIN_DNS_ZONE="main.dns.zone" + +export GITHUB_OIDC_TOKEN_HEADER_NAME="github-oidc-token-header-name" +export GITHUB_OIDC_HOSTNAME="github-oidc-hostname" +export ACTIONS_ID_TOKEN_REQUEST_URL="actions-id-token-request-url" +export ACTIONS_ID_TOKEN_REQUEST_TOKEN="actions-id-token-request-token" +export GITHUB_REPOSITORY="github-repository" + +export ENVOY_PROXY_IMAGE="envoy-proxy-image:version" +export ENVOY_SERVICE_NAME="envoy-service-name" +export ENVOY_EXTRA_ARGS="--extra-args" +export PROXY_LOG_LEVEL="debug" + +export DYNAMIC_PROXY_PORT="443" +export PROXY_PORT="8080" +export WEBSOCKETS_PROXY_PORT="9443" +export PATH_CERTS_DIR="/path/to/certs" + +docker compose config diff --git a/actions/setup-gap/scripts/debug-render-template.sh b/actions/setup-gap/scripts/debug-render-template.sh new file mode 100755 index 00000000..73d516a2 --- /dev/null +++ b/actions/setup-gap/scripts/debug-render-template.sh @@ -0,0 +1,13 @@ +#! /bin/bash + +export MAIN_DNS_ZONE="main.dns.zone" +export GITHUB_OIDC_TOKEN_HEADER_NAME="github-oidc-token-header-name" +export PROXY_PORT="8080" +export K8S_API_ENDPOINT_PORT="6443" +export DYNAMIC_PROXY_PORT="443" +export AUTH_SERVICE_NAME="auth-service-name" +export AUTH_SERVICE_PORT="9001" +export WEBSOCKETS_SERVICES="websocket-service-1,websocket-service-2" +export WEBSOCKETS_PROXY_PORT="9443" + +gomplate -V -f envoy.yaml.gotmpl -o envoy-debug.yaml