crowdsec-bouncer-traefik-plugin icon indicating copy to clipboard operation
crowdsec-bouncer-traefik-plugin copied to clipboard

[BUG] Traefik v3 does not render correctly html ban page

Open aikooo7 opened this issue 2 months ago • 13 comments

Describe the bug 🐛 When using the provided ban.html and providing it with the option, it gets printed even through the html renders if you test it in a html renderer.

Expected behavior 👀 The html getting rendered.

Context 🔎

Configuration:

- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=APIKEY"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.logLevel=DEBUG"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.banHTMLFilePath=/ban.html"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiHost=crowdsec:8080"

No error in logs were found.

Version (please complete the following information):

  • OS: Docker
  • Traefik version: 3.0.0
  • Plugin version: 1.3.0
  • Redis: ?

To Reproduce Steps to reproduce the behavior:

  1. Put configuration.
  2. Activate middleware.
  3. Ban yourself.
  4. Try to go to the service.

aikooo7 avatar May 05 '24 12:05 aikooo7

I suspect that this might be the change in v3 that causes this: https://doc.traefik.io/traefik/master/migration/v2-to-v3/#content-type-auto-detection

I just ran into the same thing upgrading.

benscobie avatar May 05 '24 16:05 benscobie

Hi @aikooo7,

I've tried to reproduce with the minimal exemple custom-ban-page updated recently to version 3 of Traefik.

make run_custombanpage image

I then went to http://localhost:8000/foo/ to get my IP from whoami and banned it in crowdsec container.
after reloading /foo, I had the ban template correctly rendered.

image

The debug logs I have are the following:

DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:12:22 getTLSConfigCrowdsec:CrowdsecLapiScheme https:no
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:12:22 cache:New initialized isRedis:false
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:12:22 New initialized mode:live
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:12:31 ServeHTTP ip:172.18.0.1 isTrusted:false
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:12:31 cache:Get key:172.18.0.1
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:12:31 ServeHTTP:Get ip:172.18.0.1 isBanned:false cache:miss
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:12:31 cache:Set key:172.18.0.1 value:f duration:60s
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:13:50 ServeHTTP ip:172.18.0.1 isTrusted:false
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:13:50 cache:Get key:172.18.0.1
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:13:50 ServeHTTP:Get ip:172.18.0.1 isBanned:false cache:miss
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:13:50 cache:Set key:172.18.0.1 value:t duration:60s
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:13:50 ServeHTTP:handleNoStreamCache ip:172.18.0.1 isBanned:t handleNoStreamCache:banned
DEBUG: CrowdsecBouncerTraefikPlugin: 2024/05/06 16:13:50 handleRemediationServeHTTP ip:172.18.0.1 remediation:t

Could you please try to test the minimal exemple custom-ban-page in the repository and see if it's working for you.

If not, I would need more information (a docker-compose and every debug logs from the plugin) to try and reproduce.

mathieuHa avatar May 06 '24 16:05 mathieuHa

Hey @mathieuHa, I appreciate your help a lot but the problem with the demo is that it conflicts with my configuration, even when I change the name on the docker compose it just turns out to be a mess.

Any other way I could fix this?

Here is a screenshot of my problem in case it helps:

Screenshot_20240506-234021_Firefox.png

aikooo7 avatar May 06 '24 22:05 aikooo7

Wanted to add on that I am experiencing this problem as well, though the demo works correctly and renders the ban.html page correctly, on my own installation, it does not and instead renders the html as text, inspecting the page seems to shows that the html page got wrapped in between "<pre></pre>" tags:

image

Still investigating this problem though I am also using Cloudflare in front as well which could have other potential stuff causing an issue, but i do have all the Auto-Minify and Rocket Loader off.

Using Traefik V3 and V1.3 Plugin.

xKhronoz avatar May 07 '24 10:05 xKhronoz

Still investigating this problem though I am also using Cloudflare in front as well which could have other potential stuff causing an issue, but i do have all the Auto-Minify and Rocket Loader off.

Apparently cloudflare is not the problem since I am not using it.

aikooo7 avatar May 07 '24 11:05 aikooo7

Hey all,

@benscobie: I don't think, it's related because in the code we have:

func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
	rw.WriteHeader(http.StatusForbidden)
	if bouncer.banTemplateString != "" {
		rw.Header().Set("Content-Type", "text/html; charset=utf-8")
		fmt.Fprint(rw, bouncer.banTemplateString)
	}
}

We set the content type if the BanHTMLFilePath is set.

@xKhronoz: Could you share your docker-compose config, and debug logs from the launch of Traefik with our plugin. Maybe try to put our plugin in the last middleware execution list.

maxlerebourg avatar May 07 '24 11:05 maxlerebourg

Hey all,

@benscobie: I don't think, it's related because in the code we have:

func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
	rw.WriteHeader(http.StatusForbidden)
	if bouncer.banTemplateString != "" {
		rw.Header().Set("Content-Type", "text/html; charset=utf-8")
		fmt.Fprint(rw, bouncer.banTemplateString)
	}
}

If I understand correctly and you mean it is related to cloudflare then why would I get it?

aikooo7 avatar May 07 '24 12:05 aikooo7

I absolutely don't know why you get it, I just said that the contentType is not the problem there. If you want help, give me your config to reproduce it locally. We are totally blind right now, we have two screenshots of ban.html and 5 lines of labels. How can we help you ...

maxlerebourg avatar May 07 '24 12:05 maxlerebourg

Hey all,

@benscobie: I don't think, it's related because in the code we have:

func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
	rw.WriteHeader(http.StatusForbidden)
	if bouncer.banTemplateString != "" {
		rw.Header().Set("Content-Type", "text/html; charset=utf-8")
		fmt.Fprint(rw, bouncer.banTemplateString)
	}
}

We set the content type if the BanHTMLFilePath is set.

@xKhronoz: Could you share your docker-compose config, and debug logs from the launch of Traefik with our plugin. Maybe try to put our plugin in the last middleware execution list.

I might have found the root cause of my issue, at least with regards to the way I setup Traefik, Crowdsec, and my other service, as I separate my services into their own docker-compose files depending on their use case, with the use of the 'Dynamic' Configuration File to setup a few common Middlewares that are shared across my services and the use of 'Chain' Middleware to combine these middleware into one, including this plugin.

Saw that @benscobie mentioned that it could be related to the new Traefik V3 update not supporting content type auto detection by default and decided to investigate a bit. After some tinkering around adding in the auto content type detection middleware finally got the html page to be rendered correctly for me ✅ at least. Although I am not 100% sure if the problem happens due to there being more than 1 middleware in used for a service and somehow the content type set by the plugin is not being forwarded or something?

A few variables to tested, (will update with more if I have time) EDITED:

  1. This Plugin + other Middlewares in a Chain Middleware [✅] (My Config, Middleware Chain is setup globally under entrypoints)
  2. This Plugin + other Middlewares, No Chain Middleware [✅] (Tested to be working from config posted by @aikooo7)
  3. This Plugin alone as Middleware [✅] (From Minimal Example: Confirmed to be working)

@maxlerebourg and yes I had tried to move the plugin to the last in my middleware chain but it did not work before, however after adding the auto content type detection middleware and moving the plugin middleware to the last in the chain as well did the trick.

TLDR Solution (if you are using multiple Middleware / Middleware Chain):

Setup and add the auto detection of content type middleware and move this plugin to the last in the list/chain.

More info to setting that middleware can be found here: https://doc.traefik.io/traefik/middlewares/http/contenttype/

Middleware Chain Ordering

image

My Dynamic config.yaml

Dynamic config.yaml


# Dynamic Config File

# TLS Config
tls:
  options:
    default:
      minVersion: VersionTLS12
      sniStrict: true
      cipherSuites:
        # TLS 1.3
        - "TLS_AES_256_GCM_SHA384"
        - "TLS_AES_128_GCM_SHA256"
        - "TLS_CHACHA20_POLY1305_SHA256"

        # TLS 1.2
        - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"
        - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
        - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
        - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
        - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"

        # Downgrade attack prevention (RFC 7507)
        - "TLS_FALLBACK_SCSV"
      curvePreferences:
        - CurveP521
        - CurveP384
      clientAuth:
        caFiles:
          # mTLS with CF Origin CA
          - /authenticated_origin_pull_ca.pem
        clientAuthType: RequireAndVerifyClientCert
    mintls13:
      minVersion: VersionTLS13
      sniStrict: true

# HTTP Config
http:
  middlewares:
    security-headers:
      headers:
        frameDeny: true
        browserXssFilter: true
        contentTypeNosniff: true
        hostsProxyHeaders: X-Forwarded-Host
        sslProxyHeaders:
          X-Forwarded-Proto: https
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 63072000 # 2 Years https://hstspreload.org/
        referrerPolicy: same-origin
        customFrameOptionsValue: SAMEORIGIN
        customRequestHeaders:
          X-Forwarded-Proto: https
        customResponseHeaders:
          X-Robots-Tag: "noindex, nofollow, nosnippet, noarchive, notranslate, noimageindex"

    autodetect-contenttype:
      contentType: {}

    rate-limiter:
      rateLimit:
        average: 100
        burst: 50

    crowdsec-bouncer:
      plugin:
        crowdsec-bouncer-traefik-plugin:
          Enabled: true
          LogLevel: INFO
          CrowdsecLapiHost: crowdsec:8080
          CrowdsecLapiKey: REDACTED
          CrowdsecMode: stream
          UpdateIntervalSeconds: 10
          UpdateMaxFailure: 5
          ForwardedHeadersTrustedIPs:
            # CF IP Range
            - 103.21.244.0/22
            - 103.22.200.0/22
            - 103.31.4.0/22
            - 104.16.0.0/13
            - 104.24.0.0/14
            - 108.162.192.0/18
            - 131.0.72.0/22
            - 141.101.64.0/18
            - 162.158.0.0/15
            - 172.64.0.0/13
            - 173.245.48.0/20
            - 188.114.96.0/20
            - 190.93.240.0/20
            - 197.234.240.0/22
            - 198.41.128.0/17
            - 2400:cb00::/32
            - 2606:4700::/32
            - 2803:f800::/32
            - 2405:b500::/32
            - 2405:8100::/32
            - 2a06:98c0::/29
            - 2c0f:f248::/32
          ClientTrustedIPs:
            - 127.0.0.1/32 # LocalHost
            - 10.0.0.0/8 # Private IP Range
            - 192.168.0.0/16 # Private IP Range
            - 172.16.0.0/12 # Private IP Range
            - 100.64.0.0/10 # Tailscale IP Range
          BanHTMLFilePath: /ban.html
          RedisCacheEnabled: true
          RedisCacheHost: crowdsec-traefik-bouncer-redis:6379
          RedisCachePassword: REDACTED
          RedisCacheDatabase: "5"

    # For External Access Services
    external-secured:
      chain:
        middlewares:
          - rate-limiter
          - autodetect-contenttype
          - security-headers
          - crowdsec-bouncer

Let me know if you need more information, I will post my other configs.

Would be great if others can confirm whether my solution works for them as well as report how their setup/config are! 😀

xKhronoz avatar May 07 '24 14:05 xKhronoz

I absolutely don't know why you get it, I just said that the contentType is not the problem there. If you want help, give me your config to reproduce it locally. We are totally blind right now, we have two screenshots of ban.html and 5 lines of labels. How can we help you ...

I am just as lost as you, if the xKhronoz's solution doesn't work I will share my config here

aikooo7 avatar May 07 '24 17:05 aikooo7

Hey, the xKhronoz's solution worked, thank you so much!

I can also confirm I had multiple middlewares. If further configuration is needed please tell me and I will try to help.

aikooo7 avatar May 07 '24 17:05 aikooo7

Hi, Thanks for posting. I will keep this issue open for now as many people will upgrade to v3 might encounter this bug.
I have to dig a bit more to see in which case autodetect-contenttype is needed.

If you have exemples of config that had this issue, please feel free to post here.
@aikooo7 It would help to have at least what middleware you use for a service

After gathering some conf, I will try to update the documentation and the examples.

mathieuHa avatar May 07 '24 20:05 mathieuHa

@aikooo7 It would help to have at least what middleware you use for a service

After gathering some conf, I will try to update the documentation and the examples.

Here are the middlewares I am using:

- "traefik.http.routers.vaultwarden.middlewares=security-headers,autodetect,crowdsec"

Security-headers I use everywhere, autodetect and crowdsec are for crowdsec.

Here are there definition:

- "traefik.http.middlewares.security-headers.headers.accesscontrolmaxage=100"
- "traefik.http.middlewares.security-headers.headers.addvaryheader=true"
- "traefik.http.middlewares.security-headers.headers.hostsproxyheaders=X-Forwarded-Host"
- "traefik.http.middlewares.security-headers.headers.sslredirect=true"
- "traefik.http.middlewares.security-headers.headers.sslproxyheaders.X-Forwarded-Proto:https"
- "traefik.http.middlewares.security-headers.headers.stsseconds=63072000"
- "traefik.http.middlewares.security-headers.headers.stsincludesubdomains=true"
- "traefik.http.middlewares.security-headers.headers.forcestsheader=true"
- "traefik.http.middlewares.security-headers.headers.framedeny=true"
- "traefik.http.middlewares.security-headers.headers.contenttypenosniff=true"
- "traefik.http.middlewares.security-headers.headers.browserxssfilter=true"
- "traefik.http.middlewares.security-headers.headers.referrerpolicy=same-origin"
- "traefik.http.middlewares.security-headers.headers.featurepolicy=camera 'none'; geolocation 'none'; microphone 'none'; payment 'none'; usb 'none'; vr 'none'"
- "traefik.http.middlewares.security-headers.headers.customresponseheaders.X-Robots-Tag=none,noarchive,nosnippet,notranslate,noimageindex"
- "traefik.http.middlewares.autodetect.contenttype=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.enabled=true"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdseclapikey=APIKEY"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.banHTMLFilePath=/ban.html"
- "traefik.http.middlewares.crowdsec.plugin.bouncer.crowdsecLapiHost=crowdsec:8080"

aikooo7 avatar May 07 '24 20:05 aikooo7

Hey all, I just find that the security headers that is problematic here is contentTypeNosniff: true If you remove it, the ban page is displayed as usual. Then the set Content-Type here does not work as expected:

func handleBanServeHTTP(bouncer *Bouncer, rw http.ResponseWriter) {
	rw.WriteHeader(http.StatusForbidden)
	if bouncer.banTemplateString != "" {
		rw.Header().Set("Content-Type", "text/html; charset=utf-8")
		fmt.Fprint(rw, bouncer.banTemplateString)
	}
}

You were right, I was blind to my own mistake 🥲. Thanks a lot all, I will make a PR to fix it. See you next release 👍

maxlerebourg avatar May 16 '24 09:05 maxlerebourg

Release v1.3.1 is here, with a fix

mathieuHa avatar May 16 '24 18:05 mathieuHa