oauth2-proxy: Errors Middleware cannot be used without an insecure workaround

There are numerous issues made regarding issues with Traefik and oauth2-proxy, (see: #1639 #1538, #1297, #1255, #1023 etc) this is in part due to the fact that the errors middleware is being used in a way that goes against Traefik’s own documentation (See: https://github.com/traefik/traefik/issues/7872#issuecomment-778202397 ). Part of what the errors middleware does is that it creates it’s own page that is shown to the client, however this then overwrites the headers that were passed from the original request to the errors middleware, leaving the redirection url to not be correct. The errors middleware creates numerous issues and needs to either be removed as a configuration option, or oauth2-proxy needs to be refactored in order to take in the header that Traefik passes directly to it on it’s forward auth request. Adding insult to injury, one of the workarounds that doesn’t involve messing around with templates requires that one trusts all X-Forwarded headers globally, which is a SIGNIFICANT security risk, and could compromise the protection Traefik is meant to provide.

See the below traefik log, showing the request being made:

traefik               | time="2022-08-22T14:53:46-04:00" level=debug msg="vulcand/oxy/roundrobin/rr: completed ServeHttp on request" Request="{\"Method\":\"GET\",\"URL\":{\"Scheme\":\"\",\"Opaque\":\"\",\"User\":null,\"Host\":\"\",\"Path\":\"/oauth2/auth/\",\"RawPath\":\"\",\"OmitHost\":false,\"ForceQuery\":false,\"RawQuery\":\"\",\"Fragment\":\"\",\"RawFragment\":\"\"},\"Proto\":\"HTTP/2.0\",\"ProtoMajor\":2,\"ProtoMinor\":0,\"Header\":{\"Accept\":[\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\"],\"Accept-Encoding\":[\"gzip\"],\"Accept-Language\":[\"en-US,en;q=0.9\"],\"Cdn-Loop\":[\"cloudflare; loops=1\"],\"Cf-Connecting-Ip\":[\"<REDACTED>\"],\"Cf-Ipcountry\":[\"US\"],\"Cf-Ray\":[\"<REDACTED>"],\"Cf-Visitor\":[\"{\\\"scheme\\\":\\\"https\\\"}\"],\"Cookie\":[\"<REDACTED>"],\"Sec-Ch-Ua\":[\"\\\"Chromium\\\";v=\\\"104\\\", \\\" Not A;Brand\\\";v=\\\"99\\\", \\\"Microsoft Edge\\\";v=\\\"104\\\"\"],\"Sec-Ch-Ua-Mobile\":[\"?0\"],\"Sec-Ch-Ua-Platform\":[\"\\\"Windows\\\"\"],\"Sec-Fetch-Dest\":[\"document\"],\"Sec-Fetch-Mode\":[\"navigate\"],\"Sec-Fetch-Site\":[\"none\"],\"Sec-Fetch-User\":[\"?1\"],\"Upgrade-Insecure-Requests\":[\"1\"],\"User-Agent\":[\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.47\"],\"X-Forwarded-Host\":[\"oauth.<REDACTED>.org\"],\"X-Forwarded-Port\":[\"443\"],\"X-Forwarded-Proto\":[\"https\"],\"X-Forwarded-Server\":[\"<REDACTED>"],\"X-Real-Ip\":[\"<REDACTED>"]},\"ContentLength\":0,\"TransferEncoding\":null,\"Host\":\"oauth.<REDACTED>.org\",\"Form\":null,\"PostForm\":null,\"MultipartForm\":null,\"Trailer\":null,\"RemoteAddr\":\"<REDACTED>:62252\",\"RequestURI\":\"/oauth2/auth/\",\"TLS\":null}"
traefik               | time="2022-08-22T14:53:46-04:00" level=debug msg="Remote error https://oauth.<REDACTED>.org/oauth2/auth/. StatusCode: 403" middlewareName=oauth-auth@file middlewareType=ForwardedAuthType
traefik               | time="2022-08-22T14:53:48-04:00" level=debug msg="vulcand/oxy/roundrobin/rr: Forwarding this request to URL" ForwardURL="http://172.18.0.4:4180" Request="{\"Method\":\"GET\",\"URL\":{\"Scheme\":\"\",\"Opaque\":\"\",\"User\":null,\"Host\":\"\",\"Path\":\"/oauth2/start\",\"RawPath\":\"\",\"OmitHost\":false,\"ForceQuery\":false,\"RawQuery\":\"rd=https%3A%2F%2Foauth.<REDACTED>.org%2F\",\"Fragment\":\"\",\"RawFragment\":\"\"},\"Proto\":\"HTTP/2.0\",\"ProtoMajor\":2,\"ProtoMinor\":0,\"Header\":{\"Accept\":[\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\"],\"Accept-Encoding\":[\"gzip\"],\"Accept-Language\":[\"en-US,en;q=0.9\"],\"Cdn-Loop\":[\"cloudflare\"],\"Cf-Connecting-Ip\":[\"<REDACTED>\"],\"Cf-Ipcountry\":[\"US\"],\"Cf-Ray\":[\"7<REDACTED>\"],\"Cf-Visitor\":[\"{\\\"scheme\\\":\\\"https\\\"}\"],\"Cookie\":[\"_oauth2_proxy_csrf=<REDACTED>\"],\"Referer\":[\"https://whoami.<REDACTED>.org/\"],\"Sec-Ch-Ua\":[\"\\\"Chromium\\\";v=\\\"104\\\", \\\" Not A;Brand\\\";v=\\\"99\\\", \\\"Microsoft Edge\\\";v=\\\"104\\\"\"],\"Sec-Ch-Ua-Mobile\":[\"?0\"],\"Sec-Ch-Ua-Platform\":[\"\\\"Windows\\\"\"],\"Sec-Fetch-Dest\":[\"document\"],\"Sec-Fetch-Mode\":[\"navigate\"],\"Sec-Fetch-Site\":[\"same-origin\"],\"Sec-Fetch-User\":[\"?1\"],\"Upgrade-Insecure-Requests\":[\"1\"],\"User-Agent\":[\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.47\"],\"X-Forwarded-Host\":[\"**whoami.<REDACTED>.org**\"],\"X-Forwarded-Port\":[\"443\"],\"X-Forwarded-Proto\":[\"https\"],\"X-Forwarded-Server\":[\"<REDACTED>\"],\"X-Real-Ip\":[\"<REDACTED>\"]},\"ContentLength\":0,\"TransferEncoding\":null,\"Host\":\"whoami.<REDACTED>.org\",\"Form\":null,\"PostForm\":null,\"MultipartForm\":null,\"Trailer\":null,\"RemoteAddr\":\"<REDACTED>:53834\",\"RequestURI\":\"/oauth2/start?rd=https%3A%2F%2Foauth.<REDACTED>.org%2F\",\"TLS\":null}"
oauth-forwardauth     | <REDACTED> - d2107b65-d30b-4b5b-86f6-30262e75800e - - [2022/08/22 18:53:48] whoami.<REDACTED>.org GET - "/oauth2/start?rd=https%3A%2F%2Foauth.<REDACTED>.org%2F" HTTP/1.1 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36 Edg/104.0.1293.47" 302 493 0.000

Expected Behavior

oauth2-proxy should be getting the redirect url from the request traefik makes to it over port 4180, not on a separate request made before the forward authentication request. Unless that change is made, dynamic configuration using the errors middleware is not only not possible, but potentially could expose unknowing users to significant security risks. oauth2-proxy should be able to dynamically assign the rd value by parsing the request made by traefik. (NOTE: This solution would break if the provider button is skipped. As we don’t stay on the original url before the forward auth request is made)

As it stands, the errors middleware is providing a significant amount of confusion and needless troubleshooting for those on traefik. The errors middleware configuration should be removed from documentation, and the example configuration should be update to include a similar setup to https://github.com/oauth2-proxy/oauth2-proxy/issues/1639#issuecomment-1224763241

Current Behavior

oauth2-proxy only redirects to its own host address, unless insecure headers are enabled globally when using the errors middleware, which is presented as the primary configuration option for Traefik

Possible Solution

Dynamically parse and then fill the rd value by reading the X-Forwarded-Host header from the request sent by Traefik to oauth2-proxy

Remove all mention of the errors middleware, and replace it with a similar configuration to the one in https://github.com/oauth2-proxy/oauth2-proxy/issues/1639#issuecomment-1224763241

Steps to Reproduce (for bugs)

See: #1538 , #1297 , #1255 , and #1639

My setup is currently using the following for Static config, dynamic config, and config.cfg (for oauth2-proxy)

Dynamic Config

http:
  middlewares:
    oauth-chain:
      chain:
        middlewares:
          - oauth-auth
          - oauth-errors
          - less-secure-headers
    oauth-lessersec:
      chain:
        middlewares:
          - oauth-auth
          - oauth-errors
          - lesser-secure-headers
    test-secure:
      chain:
        middlewares:
          - traefik-auth
          - secure-headers
          - default-compress
    secure-link:
      chain:
        middlewares:
          - less-secure-headers
          - default-compress
    default-compress:
      compress: {}
    traefik-auth:
      basicauth:
        users:
          - <REDACTED>
    secure-headers:
      headers:
        sslforcehost: true
        browserxssfilter: true
        frameDeny: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 63072000
        contentTypeNosniff: true
        accessControlAllowMethods:
          - GET
          - POST
        accessControlAllowOriginList:
          - https://<REDACTED>.org
          - https://www.<REDACTED>.org
        customresponseheaders:
          X-Robots-Tag:
            - noindex
            - nofollow
            - nosnippet
            - noarchive
            - notranslate
            - noimageindex
        accessControlMaxAge: 100
        forcestsheader: true
        addVaryheader: true
        contentSecurityPolicy: script-src 'self'
        referrerPolicy: origin-when-cross-origin
    less-secure-headers:
      headers:

        browserxssfilter: true
        #frameDeny: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 63072000
        contentTypeNosniff: true
        accessControlAllowMethods:
          - GET
          - POST
        accessControlAllowOriginList:
          - https://<REDACTED>.org
          - https://www.<REDACTED>.org
        customresponseheaders:
          X-Robots-Tag:
            - noindex
            - nofollow
            - nosnippet
            - noarchive
            - notranslate
            - noimageindex
        accessControlMaxAge: 100
        addVaryheader: true
        referrerPolicy: origin-when-cross-origin

    lesser-secure-headers:
      headers:

        browserxssfilter: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 63072000
        contentTypeNosniff: true
        customresponseheaders:
          X-Robots-Tag:
            - noindex
            - nofollow
            - nosnippet
            - noarchive
            - notranslate
            - noimageindex
        accessControlMaxAge: 100
        addVaryheader: true
      
    oauth-auth:
      forwardAuth:
        address: https://oauth.<REDACTED>.org/oauth2/auth/
        trustForwardHeader: true
        AuthResponseHeaders:
          - X-Auth-Request-Access-Token
          - Authorization
          - X-Auth-Request-User
          - X-Auth-Request-Email
          - Set-Cookie
          - X-Auth-User
          - X-Secret
          - X-Forwarded-User 
          - X-WebAuth-User
    oauth-errors:
      errors:
        status:
          - "401-403"
        service: oauth2-proxy@docker
        query: "/oauth2/sign_in"
tls:
  options:
    default:
      minVersion: VersionTLS12
      cipherSuites:
       - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
       - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
       - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
       - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
       - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
       - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
      sniStrict: true

Static config

api:
  dashboard: true
  debug: true
entryPoints:
  http:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https
  https:
    address: ":443"
    http:
      tls:
        certResolver: cloudflare
        domains:
          - main: "<REDACTED>.org"
            sans: ["*.<REDACTED>.org"]
serversTransport:
  insecureSkipVerify: true
log:
  level: 'DEBUG'
providers:
  docker:
    endpoint: "tcp://socket-proxy:<REDACTED>"
    exposedByDefault: false
    network: services
    constraints: "Label(`ai.ix.expose`, `true`)"
  file:
    directory: /config/
    watch: true
certificatesResolvers:
  cloudflare:
    acme:
      email:<REDACTED>
      storage: acme.json
      dnsChallenge:
        provider: cloudflare
        delayBeforeCheck: "60"
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"

Oauth2-proxy Config

http_address="0.0.0.0:4180"
reverse_proxy="true"
cookie_secret="<REDACTED>"
email_domains="*"
cookie_secure="true"
cookie_domains=[".<REDACTED>"]
whitelist_domains=[".<REDACTED>"]
provider="keycloak-oidc"
client_secret="<REDACTED>"
client_id="oauth2"
redirect_url="https://oauth.<REDACTED>.org/oauth2/callback"
oidc_issuer_url="https://keycloak.<REDACTED>.org/realms/master"
upstreams="static://202"
pass_access_token=true
pass_authorization_header=true
set_authorization_header=true
set_xauthrequest=true
pass_user_headers=true
cookie_httponly=false

Docker Compose Configuration for Traefik, OAuth2-proxy, Whoami, keycloak

traefik:
      image: traefik:vacherin
      container_name: traefik
      restart: unless-stopped
      security_opt:
        - no-new-privileges:true
      networks:
        - internal
        - services
        - auth
      ports:
        - 80:80
        - 443:443

      environment:
        - CF_API_EMAIL=<REDACTED>
        #- CF_API_KEY=<REDACTED>
        - CF_DNS_API_TOKEN=<REDACTED>
      volumes:
        - /etc/localtime:/etc/localtime:ro
        - /var/run/docker.sock:/var/run/docker.sock:ro
        - ./Traefik/data/traefik.yml:/traefik.yml:ro
        - ./Traefik/acme/acme.json:/acme.json
        - ./Traefik/data/:/config/:ro
        - ./Traefik/data/log:/var/log/traefik
      labels:
      - "traefik.enable=true"
      - "ai.ix.expose=true"
      - "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
      - "traefik.http.routers.traefik-secure.entrypoints=https"
      - "traefik.http.routers.traefik-secure.rule=Host(`traefik.<REDACTED>.org`)"
      - "traefik.http.routers.traefik-secure.middlewares=oauth-chain@file"
      - "traefik.http.routers.traefik-secure.tls=true"
      - "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
      - "traefik.http.routers.traefik-secure.tls.domains[0].main=<REDACTED>.org"
      - "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.<REDACTED>.org"
      - "traefik.http.routers.traefik-secure.service=api@internal"
oauth2-proxy:
    image: bitnami/oauth2-proxy:7
    container_name: oauth-forwardauth
    restart: unless-stopped
    networks:
      services:
        aliases:
          - oauth.local.<REDACTED>.org
      auth:

    volumes:
      - ./oauth2/config.cfg:/config.cfg:ro
    command: 
      - "--config=/config.cfg"
      - "--code-challenge-method=S256"
    labels:
      - "traefik.enable=true"
      - "ai.ix.expose=true"
      - "traefik.http.routers.oauth2-proxy.middlewares=less-secure-headers@file"
      - "traefik.http.routers.oauth2-proxy.entrypoints=https"
      - "traefik.http.routers.oauth2-proxy.rule=Host(`oauth.<REDACTED>.org`) || PathPrefix(`/oauth2`)"
      - "traefik.http.routers.oauth2-proxy.tls=true"
      - "traefik.http.routers.oauth2-proxy.tls.certresolver=cloudflare"
      - "traefik.http.routers.oauth2-proxy.tls.domains[0].main=<REDACTED>.org"
      - "traefik.http.routers.oauth2-proxy.tls.domains[0].sans=*.<REDACTED>.org"
      - "traefik.http.routers.oauth2-proxy.service=oauth2-proxy@docker"
      - "traefik.http.services.oauth2-proxy.loadbalancer.server.port=4180"
      #- "traefik.http.services.oauth2-proxy.loadbalancer.passHostHeader=false"
    depends_on:
      keycloak:
        condition: service_healthy
  keycloak:
    image: bitnami/keycloak:18
    container_name: keycloak
    restart: unless-stopped
    volumes: 
      - /etc/localtime:/etc/localtime:ro
    networks:
      services:
      auth:
        aliases:
          - keycloak.local.<REDACTED>.org
      keycloakDB:
    depends_on:
        KCPostgres:
          condition: service_healthy
    env_file: ./Keycloak/config.env
    labels:
      - "traefik.enable=true"
      - "ai.ix.expose=true"
      - "traefik.http.routers.keycloak.rule=Host(`keycloak.<REDACTED>.org`)"
      - "traefik.http.routers.keycloak.entrypoints=https"
      - "traefik.http.routers.keycloak.middlewares=secure-link@file"
      - "traefik.http.routers.keycloak.tls=true"
      - "traefik.http.routers.keycloak.tls.certresolver=cloudflare"
      - "traefik.http.routers.keycloak.tls.domains[0].main=<REDACTED>.org"
      - "traefik.http.routers.keycloak.tls.domains[0].sans=*.<REDACTED>.org"
      - "traefik.http.services.keycloak.loadbalancer.server.port=8080"
      - "traefik.http.routers.keycloak.service=keycloak@docker"
    healthcheck:
      test: ["CMD-SHELL", "curl --fail http://localhost:8080/realms/master || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 10
      start_period: 1m

  whoami:
    image: "traefik/whoami"
    networks:
      - services
    labels:
      - "traefik.enable=true"
      - "ai.ix.expose=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.<REDACTED>.org`)"
      - "traefik.http.routers.whoami.entrypoints=https"
      - "traefik.http.routers.whoami.tls=true"
      - "traefik.http.routers.whoami.middlewares=oauth-lessersec@file"
      - "traefik.http.services.whoami.loadbalancer.server.port=80"
      - "traefik.http.routers.whoami.tls.certresolver=cloudflare"
      - "traefik.http.routers.whoami.tls.domains[0].main=<REDACTED>.org"
      - "traefik.http.routers.whoami.tls.domains[0].sans=*.<REDACTED>.org"
      - "traefik.http.routers.whoami.service=whoami@docker"
    depends_on:
      oauth2-proxy:
        condition: service_started

Context

I am attempting to use the oauth2-proxy as a forward auth provider for my reverse proxy. I want to gatekeep some of my services behind sso, while still being able to provide granular protection to various services.

Your Environment

Host OS: Proxmox VE 7.2-7 Guest OS: Almalinux 8.6 (LXC: FUSE, KEYCTL, and NESTING are enabled)

  • Version used: I am using the Bitnami container of oauth2-proxy, pinned to the 7 tag, which puts it on 7.3.0

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 19 (4 by maintainers)

Most upvoted comments

I figured it out. I was being stupid and forgot to remove the errors middleware from my chaining middleware. However, I believe that the errors middleware should be removed from documentation in order to present the correct configuration needed in order to set it up with traefik. Because as it stands, if I wasn’t already deep into the sunk cost fallacy, I would have switch away from oauth2-proxy for traefik-forward-auth or pomerium.

Do we have scope on what it would take to drop the insecure workaround and be fully supported? Are changes needed here or in Traefik?

This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.