Repository navigation
Trust forwarded headers from the ingress so links use https - #28
Conversation
uvicorn trusts X-Forwarded-* only from 127.0.0.1 by default, so behind Traefik it saw plain http and generated http:// pagination links. Set FORWARDED_ALLOW_IPS to the mmli2 pod network. A network, not "*", so the client address comes from the entry the ingress appended rather than one the client supplied.
matthewberry
left a comment
There was a problem hiding this comment.
I found no functional bug, but I have three concerns about the setting itself (inline). None of these were reproduced. The first is based on reading the installed uvicorn 0.34.2 uvicorn/middleware/proxy_headers.py.
Three more points about the PR body:
- The evidence for the /16 doesn't add up. The body lists the nodes' pod networks as
10.42.{0,2,3}.0/24, but Traefik's address,10.42.9.118, falls in none of them. Either that list is incomplete or Traefik's address comes from somewhere else, so it isn't yet shown that Traefik's source address is always inside 10.42.0.0/16. - The value being shipped wasn't tested as trusted. The trusted case was tested with
127.0.0.0/8, and nobody built the image and checked that theENVtakes effect underuv run uvicorn. The table row labeled "as production today" doesn't match production, which leaves the variable unset (so only 127.0.0.1 is trusted by default). This shows the approach works, not that this change does. - Client IPs in logs: once Traefik is trusted, the access log records each end user's IP instead of Traefik's. The body mentions this; it's a data-handling policy question rather than a code defect, but it's worth an explicit decision.
The branch is 3 commits behind main. None of them touch the Dockerfile, and main still builds links from request.url, so the fix still applies.
| EXPOSE 8000 | ||
|
|
||
| # Trust X-Forwarded-* from the ingress so generated URLs use https (mmli2 pod network; | ||
| # override for other clusters). Not "*", which would take a client-supplied X-Forwarded-For |
There was a problem hiding this comment.
From inside the cluster this works the same as "*". 10.42.0.0/16 covers every pod in every namespace, not just Traefik. A pod can call the Service directly with X-Forwarded-For: 1.2.3.4 and X-Forwarded-Proto: https, and uvicorn accepts both. When an in-cluster caller goes through the ingress, every address in the forwarded list is in 10.42.x.x. uvicorn's ProxyHeadersMiddleware walks the list from the right, and when every entry is trusted it returns the first one, which the caller chose. So the guarantee this comment gives only holds for traffic from outside the cluster. Narrowing to Traefik's address(es), or saying in the comment that in-cluster callers are trusted, would fix the mismatch.
There was a problem hiding this comment.
Agreed, and confirmed by running it: on a 10.42.0.0/16 Docker network, a direct call to the app with X-Forwarded-For: 1.2.3.4 got 1.2.3.4 logged and an https:// link. Narrowing to Traefik isn't practical, because its pod IP changes whenever it restarts. In 53d96b2 the comment (now in chart/values.mmli2.yaml) says that every pod in the cluster is trusted, not only Traefik, and that the guarantee is for outside clients.
There was a problem hiding this comment.
Agreed. Pinning Traefik isn't workable while its IP changes on restart, and the comment in values.mmli2.yaml now states the actual guarantee (outside clients only). For a ClusterIP-only Service, that tradeoff is fine.
| # Expose port | ||
| EXPOSE 8000 | ||
|
|
||
| # Trust X-Forwarded-* from the ingress so generated URLs use https (mmli2 pod network; |
There was a problem hiding this comment.
The repo's chart gives no way to do this override. With existingSecret set (the default in chart/values.yaml), chart/templates/deployment.yaml renders only envFrom: secretRef; otherwise it renders the five fixed CLEAN_DB_* variables. An operator who adds FORWARDED_ALLOW_IPS to the values has it silently dropped, and links stay http://. Today the only options are a template change or re-sealing the secret.
There was a problem hiding this comment.
Confirmed: with existingSecret set, the template rendered only envFrom. Fixed in 53d96b2 with a controller.extraEnv passthrough, rendered in both branches of the template. helm template with values.yaml is unchanged; with values.mmli2.yaml the only change is the one env entry; with existingSecret empty the five CLEAN_DB_* variables still render, followed by the extra one.
There was a problem hiding this comment.
Confirmed. I rendered 53d96b2 with helm template: values.yaml gives only envFrom, as before; values.mmli2.yaml gives envFrom plus the single FORWARDED_ALLOW_IPS entry; and with existingSecret cleared, the five CLEAN_DB_* variables render, followed by the extra one. kubectl apply --dry-run=client accepts the mmli2 deployment.
|
|
||
| # Trust X-Forwarded-* from the ingress so generated URLs use https (mmli2 pod network; | ||
| # override for other clusters). Not "*", which would take a client-supplied X-Forwarded-For | ||
| ENV FORWARDED_ALLOW_IPS="10.42.0.0/16" |
There was a problem hiding this comment.
This puts one cluster's pod network into a shared image. The comment names mmli2, but chart/values.yaml deploys to fastapi.cleandb.mmli1.ncsa.illinois.edu. If mmli1's pod network isn't 10.42.0.0/16, or either network changes later, links quietly go back to http:// and nothing signals it. Setting the variable in the chart's env, with a passthrough in the template, would keep cluster details out of the image and fix the override problem on line 35 too.
There was a problem hiding this comment.
Agreed on keeping cluster details out of the image, and done in 53d96b2: the ENV is gone from the Dockerfile, and FORWARDED_ALLOW_IPS is set in values.mmli2.yaml through the new extraEnv passthrough.
On mmli1: chart/values.yaml on main points at fastapi.cleandb.mmli2.ncsa.illinois.edu. mmli1 was there in the chart's first commit (9a98ca0) and replaced in 79c8daf. The live Ingress serves cleandb-api.platform.moleculemaker.org, which only values.mmli2.yaml lists, so that's the file Argo CD deploys; values.yaml gets extraEnv: [].
There was a problem hiding this comment.
You're right about mmli1, and I retract that part. chart/values.yaml points at fastapi.cleandb.mmli2.ncsa.illinois.edu on main and on this PR's first commit, afd4cd7, so it wasn't a stale-branch issue. I was simply wrong. Moving the value into values.mmli2.yaml resolves the rest of this comment.
Add a controller.extraEnv passthrough to the deployment template, rendered whether or not existingSecret is set, and set FORWARDED_ALLOW_IPS in values.mmli2.yaml. This keeps the cluster's pod network out of the shared image and gives deployments a way to change it. The comment now says that every pod in the cluster is trusted, not only Traefik.
|
Thanks. All three inline points are addressed in 53d96b2 (replies inline), and the description is rewritten. On the description points:
|
|
Thanks, this resolves everything from my review. The full node list puts Traefik's |
Problem
next/previouslinks (and any other URL built fromrequest.url) start withhttp://in production, for example:Traefik terminates TLS and sends
X-Forwarded-Proto: https, but uvicorn trustsX-Forwarded-*only from127.0.0.1by default (FORWARDED_ALLOW_IPS, uvicorn 0.34.2config.py), and Traefik connects from a pod address. Production answershttp://with a 301 tohttps://, so GET clients that follow redirects still work, but the links are wrong. The access log also records Traefik's address for every request instead of the client's.Fix
chart/templates/deployment.yaml: acontroller.extraEnvpassthrough, rendered both withexistingSecret(next toenvFrom) and without it (after the fiveCLEAN_DB_*variables). Before this, the chart had no way to set an extra variable.chart/values.mmli2.yaml:FORWARDED_ALLOW_IPS=10.42.0.0/16, the mmli2 cluster's pod network.chart/values.yamlgetsextraEnv: [].What gets trusted: every pod in the cluster, not only Traefik. Traefik's pod IP changes when it restarts, so it can't be pinned. For clients outside the cluster, uvicorn logs the last
X-Forwarded-Forentry outside the trusted network, which is the one the ingress appended, so they can't spoof their address. A pod inside the cluster can, by calling the Service directly with its ownX-Forwarded-For."*"would be worse: uvicorn then logs the first entry, which an outside client can set (middleware/proxy_headers.py,get_trusted_client_host).Cluster facts (read-only): production runs uvicorn 0.34.2 with
FORWARDED_ALLOW_IPSunset. The Service isClusterIPonly. All nine nodes' pod CIDRs fall inside10.42.0.0/16(10.42.{0,1,2,3,4,8,9,10,11}.0/24), including10.42.9.0/24onmmli2-worker-cpu-01, where Traefik's pod (10.42.9.118) runs. Traefik v3.7.16 runs with noforwardedHeaderssettings; I haven't verified whether it strips client-suppliedX-Forwarded-Foror appends to it, and this setting gives the right client address either way. The live Ingress servescleandb-api.platform.moleculemaker.org, which onlyvalues.mmli2.yamllists, so that's the file Argo CD uses.Testing
Chart:
helm templatewithvalues.yamlis byte-for-byte unchanged. Withvalues.mmli2.yaml, the only change is the oneenventry next to the existingenvFrom. WithexistingSecretempty, the fiveCLEAN_DB_*variables still render, followed by the extra one.helm lintpasses for both values files, andkubectl apply --dry-run=clientaccepts the deployment.Image: the CI-built
pr-28image (noFORWARDED_ALLOW_IPSin its config), run with its ownuv run uvicorncommand on a Docker network with subnet10.42.0.0/16, against a seeded Postgres. Requests came from acurlcontainer on the same network (10.42.0.5):FORWARDED_ALLOW_IPSnextlink10.42.0.0/16, set as the chart sets itX-Forwarded-Proto: https,X-Forwarded-For: 6.6.6.6, 203.0.113.9https://…203.0.113.9(the entry the proxy appended)10.42.0.0/16X-Forwarded-For: 1.2.3.4https://…1.2.3.4(in-cluster callers are trusted)http://…10.42.0.5(the caller)Deploying
This reaches production through an Argo CD sync of the chart, not a pod restart. The changed pod template rolls out a new pod by itself. After it's live: check that a
nextlink starts withhttps://, and that new access-log lines show client addresses rather than10.42.x.x.Things to know
http://(nothing breaks). Updatevalues.mmli2.yamlif it does.