anandslab / docker-traefik

Docker media and home server stack with Docker Compose, Traefik, Swarm Mode, Google OAuth2/Authelia, and LetsEncrypt
https://www.smarthomebeginner.com/
MIT License
2.9k stars 630 forks source link

Selective auth and 3rd party apps access #27

Closed robflate closed 4 years ago

robflate commented 4 years ago

I'm trying to setup 3rd party apps with some of my services. E.g;

The problem, as pointed out by @htpcBeginner here is that none of these apps support oAuth. It would be great if they did and at least a few of the developers are looking into it (e.g. LunaSea) but the fact is, some may never support it. To overcome this we can use traefik.http.routers.containername-rtr.middlewares=chain-no-auth@file to set a container to use BasicAuth, then use http://username:password@domain.comin the third party app. However, it's not ideal and the username and password may be visible in logs etc so not as secure.

My question is, using a stack like this with Traefik 2 and oAuth, is there a workaround that would allow 3rd party apps access to specific URLS or folders on the server, like /api or /opds etc? NGINX appears to have this feature. Something like;

location /sonarr {
    auth_request /auth-2;
    proxy_pass http://192.168.1.2:8989/sonarr;
    include conf.d/proxy-settings.conf;
    location /sonarr/api {
        auth_request off;
        proxy_pass http://192.168.1.2:8989/sonarr/api;
    }
}

but I'm not sure how you'd then allow access to the content that was linked, i.e a book from an OPDS server.

Other people seem to use Organizr to do this but I've not tried this so don't know how it works.

Anyway, sorry for the long post, but if anyone has a workaround to allow 3rd party app access using this stack, I'd appreciate any advice. Thanks.

robflate commented 4 years ago

The workaround that I've found is to create an additional router that uses a different middleware (one with no auth) for any service you want to expose the /api PathPrefix. This leaves OAuth in place for the rest of the service but also allows access to whatever PathPrefix is exposed. E.g.;

Sonarr Before

    labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.sonarr-rtr.entrypoints=https"
      - "traefik.http.routers.sonarr-rtr.rule=Host(`sonarr.$DOMAINNAME`)"
      - "traefik.http.routers.sonarr-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.sonarr-rtr.middlewares=chain-oauth@file"
      ## HTTP Services
      - "traefik.http.routers.sonarr-rtr.service=sonarr-svc"
      - "traefik.http.services.sonarr-svc.loadbalancer.server.port=8989"

Sonarr After

    labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.sonarr-rtr.entrypoints=https"
      - "traefik.http.routers.sonarr-rtr.priority=1"
      - "traefik.http.routers.sonarr-rtr.rule=Host(`sonarr.$DOMAINNAME`)"
      - "traefik.http.routers.sonarr-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.sonarr-rtr.middlewares=chain-oauth@file"
      ## HTTP Services
      - "traefik.http.routers.sonarr-rtr.service=sonarr-svc"
      - "traefik.http.services.sonarr-svc.loadbalancer.server.port=8989"
      ## API ROUTER - Allow access to API without auth for 3rd party app access that don't support oAuth
      ## HTTP Routers
      - "traefik.http.routers.sonarr-api-rtr.entrypoints=https"
      - "traefik.http.routers.sonarr-api-rtr.priority=99"
      - "traefik.http.routers.sonarr-api-rtr.rule=Host(`sonarr.$DOMAINNAME`) && PathPrefix(`/api`)"
      - "traefik.http.routers.sonarr-api-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.sonarr-api-rtr.middlewares=chain-no-auth@file"
      ## HTTP Services
      - "traefik.http.routers.sonarr-api-rtr.service=sonarr-svc"

Sonarr now works remotely with third party apps like LunaSea and NZB360 whilst still using OAuth for access to the rest of the service.

In your 3rd party app, just use https://sonarr.yourdomain.com and your Sonarr API key. Auth is provided by Sonarr requiring the API key to access it.

Another example is LazyLibrarian's OPDS server so you can use 3rd party ebook readers to access your library;

LazyLibrarian Before

    labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.lazylibrarian-rtr.entrypoints=https"
      - "traefik.http.routers.lazylibrarian-rtr.rule=Host(`lazylibrarian.$DOMAINNAME`)"
      - "traefik.http.routers.lazylibrarian-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.lazylibrarian-rtr.middlewares=chain-oauth@file"
      ## HTTP Services
      - "traefik.http.routers.lazylibrarian-rtr.service=lazylibrarian-svc"
      - "traefik.http.services.lazylibrarian-svc.loadbalancer.server.port=5299"

LazyLibrarian After

    labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.lazylibrarian-rtr.entrypoints=https"
      - "traefik.http.routers.lazylibrarian-rtr.priority=1"
      - "traefik.http.routers.lazylibrarian-rtr.rule=Host(`lazylibrarian.$DOMAINNAME`)"
      - "traefik.http.routers.lazylibrarian-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.lazylibrarian-rtr.middlewares=chain-oauth@file"
      ## HTTP Services
      - "traefik.http.routers.lazylibrarian-rtr.service=lazylibrarian-svc"
      - "traefik.http.services.lazylibrarian-svc.loadbalancer.server.port=5299"
      ## API ROUTER - Allow access to OPDS without auth for 3rd party app access that don't support oAuth
      ## HTTP Routers
      - "traefik.http.routers.lazylibrarian-opds-rtr.entrypoints=https"
      - "traefik.http.routers.lazylibrarian-opds-rtr.priority=99"
      - "traefik.http.routers.lazylibrarian-opds-rtr.rule=Host(`lazylibrarian.$DOMAINNAME`) && PathPrefix(`/opds`)"
      - "traefik.http.routers.lazylibrarian-opds-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.lazylibrarian-opds-rtr.middlewares=chain-no-auth@file"
      ## HTTP Services
      - "traefik.http.routers.lazylibrarian-opds-rtr.service=lazylibrarian-svc"

Then just use https://lazylibrarian.yourdomain.com/opds and the username and password set in LazyLibrarian to access your library in apps like Marvin.

I based this on a post on Stack Overflow and it works. There are a few issues such as LazyLibrarian's OPDS server not showing cover art, I think because they are linked outside the /opds path (they're in /cache).

Another issue is I wanted to use basic-auth on the 2nd routers middleware just for added security but it asks for the basic-auth username and password even when logging into the root of the service (not just /api) which defeats the purpose of SSO.

Finally, I don't really understand the security implications of this other than what I've mentioned. I'd love someone who knows more than me about this stuff to have a look and say if it's OK or if there's a much better method than this.

anandslab commented 4 years ago

I have pushed some updates to my repo. I have completed bypassing auth for radarr, sonarr, and lidarr when the request header has the API key.

This is a bit more secure than what is posted above because you are not bypassing anything that has /api in the request but only when API key (which should be private) is present.

There are several more ways to do the same thing. For example, I am by passing auth for sabnzbd using rules as command in oauth container instead. This is partly because I could not get the different traefik routers to work correctly when sabnzbd API key is present.

Doing this will require some trial and error and looking at oauth logs to figure which header to pick and how to phrase rule, while compromising as less as possible on security. This has to be done for each situation.

robflate commented 4 years ago

Apologies commenting in a closed issue.

First, thanks for the update, Authelia works great!

Regarding this issue, the updates to Sonarr, Radarr & Lidarr don't seem to work for me when using LunaSea (like NZB360 but for iOS and Android). I did a test using NZB360 on Android and it does seem to work. The difference seems to be that LunaSea just appends the API key to the URL. Looking at the OAuth logs, a request from LunaSea shows;

time="2020-05-12T14:25:09Z" level=debug msg="Authenticating request" cookies="[]" handler=Auth host=sonarr.mydomain.com method=GET rule=default source_ip=<my-public-ip> uri="/api/profile?apikey=<sonarr-api-key>"

time="2020-05-12T14:25:09Z" level=debug msg="Set CSRF cookie and redirected to provider login url" csrf_cookie="_forward_auth_csrf=<redacted>; Path=/; Domain=mydomain.com; Expires=Thu, 11 Jun 2020 14:25:09 GMT; HttpOnly; Secure" handler=Auth host=sonarr.mydomain.com login_url="https://accounts.google.com/o/oauth2/auth?client_id=<redacted>.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Foauth.mydomain.com%2F_oauth&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&state=<redacted>%3Agoogle%3Ahttps%3A%2F%2Fsonarr.mydomain.com%2Fapi%2Fprofile%3Fapikey%3D<sonarr-api-key>" method=GET rule=default source_ip=<my-public-ip> uri="/api/profile?apikey=<sonarr-api-key>"

Is there a workaround for this that doesn't involve doing what I originally did? Hopefully I'm not missing the point or doing something dumb....both are entirely possible! Thanks again for all your efforts.

Edit: Added that I successfully tested with NZB360 on Android. Added logs. Removed an incorrect statement. Added how LunaSea sends the API key

anandslab commented 4 years ago

Each app sends the request differently.

In your case, it appears that you will have to use regex matching for API key in "uri". I had trouble with sabnzbd where the API key was part of the X-Forwarded-Uri path. I had to resort to adding a rule directly in OAuth container instead of as Traefik label. For this reason sabnzbd is still behind OAuth. I have not had a chance to test the bypass rules in Authelia yet.

robflate commented 4 years ago

Thanks. I really like your solutions for the arr apps because they only bypass OAuth when the API key is present which like you say is private and therefore its own method of authentication. I do wish that I could use chain-basic-auth as opposed to chain-no-auth for an added level of security or for containers that don't use API keys (NZBGet for instance) but this results in the BasicAuth popup showing even when visiting the root of the site which breaks the SSO flow. Is there no way to only invoke BasicAuth when visting a specific PathPrefix like /api?

Just quickly, thanks so much for the tutorial and repository, it's fantastic. I have run a similar setup that just used BasicAuth for ages and was absolutely sick of filling in BasicAuth logins! With your setup I never really notice logging in anymore and so when trying to expose certain apps for use with 3rd party apps I really want to avoid it having the consequence of breaking the SSO flow.

4rk1t3k7 commented 4 years ago

Hey @htpcBeginner I know this is a bit of an older post, but I wanted to offer my 2 bits for a solution. You mentioned you couldn't get the oauth bypass to work for SABNZBD. I got it working with just a slight modification to your code. I really appreciate your tutorials, so I wanted to offer a tiny little contribution to the project :-)

    labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.sabnzbd-rtr.entrypoints=https"
      - "traefik.http.routers.sabnzbd-rtr.priority=1"
      - "traefik.http.routers.sabnzbd-rtr.rule=Host(`sabnzbd.$DOMAINNAME`)"
      - "traefik.http.routers.sabnzbd-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.sabnzbd-rtr.middlewares=chain-oauth@file"
      ## HTTP Services
      - "traefik.http.routers.sabnzbd-rtr.service=sabnzbd-svc"
      - "traefik.http.services.sabnzbd-svc.loadbalancer.server.port=8080"
      ## API ROUTER - Allow access to API without auth for 3rd party app access that don't support oAuth
      ## HTTP Routers
      - "traefik.http.routers.sabnzbd-api-rtr.entrypoints=https"
      - "traefik.http.routers.sabnzbd-api-rtr.priority=99"
      - "traefik.http.routers.sabnzbd-api-rtr.rule=Host(`sabnzbd.$DOMAINNAME`) && Query(`apikey=$SABNZBD_API_KEY`)" ## This is the only line of code I had to edit
      - "traefik.http.routers.sabnzbd-api-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.sabnzbd-api-rtr.middlewares=chain-no-auth@file"
      ## HTTP Services
      - "traefik.http.routers.sabnzbd-api-rtr.service=sabnzbd-svc"
anandslab commented 4 years ago

@4rk1t3k7 Thanks! I figured it out few days back and already implemented exactly what you suggested. :-)

shamoon commented 4 years ago

Thanks for this everyone! While we're adding small tweaks, I think you may want to include the possibility that the API key is passed via X-Api-Key header, so xxx-api-rtr rule becomes:

traefik.http.routers.radarr-api-rtr.rule=Host(`radarr.domain.com`) && (Headers(`X-Api-Key`, `$RADARR_API_KEY`) || Query(`apikey=$RADARR_API_KEY `))
1mfaasj commented 3 years ago

Hi guys, I haven't managed to get couchpotato and bazarr on bypass so far, have you? so that I can also control it with nzb360. would you mind sharing it if you've got it working? That would be great :-) @htpcBeginner @robflate

robflate commented 3 years ago

I don't use nzb360 and don't have a use case for opening the Bazarr API but I did a quick test with Postman and the following works;

Copy your API key from Bazarr > Settings > General > API Key and set it in .env E.g;

BAZARR_API_KEY=xxxxxxxxxxxxxxxxx
  bazarr:
    image: ghcr.io/linuxserver/bazarr:latest
    container_name: bazarr
    restart: unless-stopped
    networks:
      - t2_proxy
    security_opt:
      - no-new-privileges:true
    volumes:
      - /path/to/dockerdir/bazarr:/config
      - /path/to/data:/data
    environment:
      PUID: $PUID
      PGID: $PGID
      TZ: $TZ
    labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.bazarr-rtr.entrypoints=https"
      - "traefik.http.routers.bazarr-rtr.rule=Host(`bazarr.$DOMAINNAME`)"
      - "traefik.http.routers.bazarr-rtr.priority=99"
      ## Middlewares
      - "traefik.http.routers.bazarr-rtr.middlewares=chain-oauth@file" # Or whatever auth you use
      ## HTTP Services
      - "traefik.http.routers.bazarr-rtr.service=bazarr-svc"
      - "traefik.http.services.bazarr-svc.loadbalancer.server.port=6767"
      ## HTTP Routers Auth Bypass
      - "traefik.http.routers.bazarr-rtr-bypass.entrypoints=https"
      - "traefik.http.routers.bazarr-rtr-bypass.rule=Host(`bazarr.$DOMAINNAME`) && (Headers(`X-Api-Key`, `$BAZARR_API_KEY`) || Query(`apikey`, `$BAZARR_API_KEY`))"
      - "traefik.http.routers.bazarr-rtr-bypass.priority=100"
      - "traefik.http.routers.bazarr-rtr-bypass.service=bazarr-svc"
      - "traefik.http.routers.bazarr-rtr-bypass.middlewares=chain-no-auth@file"

I don't use couchpotato either but if you check their API docs you'll see what Headers/Query is required. Also, I can really recommend Radarr v3. I find it much better than CouchPotato.

1mfaasj commented 3 years ago

I don't use nzb360 and don't have a use case for opening the Bazarr API but I did a quick test with Postman and the following works;

Copy your API key from Bazarr > Settings > General > API Key and set it in .env E.g;

BAZARR_API_KEY=xxxxxxxxxxxxxxxxx
  bazarr:
    image: ghcr.io/linuxserver/bazarr:latest
    container_name: bazarr
    restart: unless-stopped
    networks:
      - t2_proxy
    security_opt:
      - no-new-privileges:true
    volumes:
      - /path/to/dockerdir/bazarr:/config
      - /path/to/data:/data
    environment:
      PUID: $PUID
      PGID: $PGID
      TZ: $TZ
    labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.bazarr-rtr.entrypoints=https"
      - "traefik.http.routers.bazarr-rtr.rule=Host(`bazarr.$DOMAINNAME`)"
      - "traefik.http.routers.bazarr-rtr.priority=99"
      ## Middlewares
      - "traefik.http.routers.bazarr-rtr.middlewares=chain-oauth@file" # Or whatever auth you use
      ## HTTP Services
      - "traefik.http.routers.bazarr-rtr.service=bazarr-svc"
      - "traefik.http.services.bazarr-svc.loadbalancer.server.port=6767"
      ## HTTP Routers Auth Bypass
      - "traefik.http.routers.bazarr-rtr-bypass.entrypoints=https"
      - "traefik.http.routers.bazarr-rtr-bypass.rule=Host(`bazarr.$DOMAINNAME`) && (Headers(`X-Api-Key`, `$BAZARR_API_KEY`) || Query(`apikey`, `$BAZARR_API_KEY`))"
      - "traefik.http.routers.bazarr-rtr-bypass.priority=100"
      - "traefik.http.routers.bazarr-rtr-bypass.service=bazarr-svc"
      - "traefik.http.routers.bazarr-rtr-bypass.middlewares=chain-no-auth@file"

I don't use couchpotato either but if you check their API docs you'll see what Headers/Query is required. Also, I can really recommend Radarr v3. I find it much better than CouchPotato.

Thank you very much, also for your quick response! I'm going to play around with it, and about couchpotato. i also give radarr a chance :-)

thiemo commented 3 years ago

Is there a way to use the api keys in the labels for bypassing auth without storing the keys in plain text in .env? Using secrets or somehow?