Protecting Ghost blog admin page with Traefik auth

Hi all,
I'm trying to harden the security of my ghost blog's admin interface. I managed it with the Traefik dashboard but seem unable to do so for the blog. The blog admin url is blog.mydomain.com/ghost

Whatever I do, I seem to be unable to protect "/ghost" with the Traefik authentication. Does anyone have a tip on what I might be doing wrong? The following are sections of the setup I tried

traefik.yaml

http:
  routers:
    dashboard:
      rule: Host(`traefik.mydomain.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
      service: api@internal
      middlewares:
        - auth
    rtr-blog-admin:
      rule: Host(`blog.mydomain.com`) && PathPrefix(`/ghost`)
      service: svc-blog-admin
      middlewares:
        - auth
  services:
    svc-blog-admin:
      loadBalancer:
        servers:
          - url: "https://blog.mydomain.com/ghost"

  middlewares:
    auth:
      digestAuth:
        users:
          - "myuser:traefik:xxxxxxxxxxxxx"

docker-compose.yaml (blog service label section)

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.blog.rule=Host(`blog.mydomain.com`)"
      - "traefik.http.routers.blog.entrypoints=websecure"
      - "traefik.http.routers.blog.tls.certresolver=production"

Thanks for any advice you may have.

Hi @ndy3yju1,
Thanks for your interest in Traefik.

To me, your blog admin panel URL is weird in your service. It seems you are pointing to the
same URL than the incoming request which could lead to unexpected behaviors.
Don't you want to use the real service url (e.g: https://my-blog-url-in-docker)?

If it does not help, could you provide your docker-compose file?

Thanks,
Maxence

Hi @moutoum,
Thanks for your help. I am still in the early stages of learning how traefik works. I got a few things working but I'm still confused about the interaction and syntax of the different "blocks".

In anycase, here is a somewhat redacted copy of my docker file and traefik file.

docker-compose.yaml

services:
  blog:
    container_name: blog
    hostname: blog
    image: ghost:5-alpine
    restart: always
    depends_on:
      - blog_db
    environment:
      # redacted
      NODE_ENV: production
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.blog.rule=Host(`blog.mydomain.com`)"
      - "traefik.http.routers.blog.entrypoints=websecure"
      - "traefik.http.routers.blog.tls.certresolver=production"
    volumes:
      - blog-content:/var/lib/ghost/content
    networks:
      - blog
  blog_db:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password
    container_name: blog_db
    restart: always
    environment:
      # redacted
    volumes:
      - blog-db:/var/lib/mysql
    networks:
      - blog

traefik.yml

global:
  checkNewVersion: false
  sendAnonymousUsage: true

log:
  level: INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL

accessLog:
  filePath: "/var/log/traefik/access.log"
  format: json

api:
  dashboard: true
  insecure: false

http:
  routers:
    dashboard:
      rule: # redacted
      service: api@internal
      middlewares:
        - auth
    rtr-blog-admin:
      rule: Host(`blog.mydomain.com`) && PathPrefix(`/ghost`)
      service: svc-blog-admin
      middlewares:
        - auth
  services:
    svc-blog-admin:
      loadBalancer:
        servers:
          - url: "https://blog/ghost"

entryPoints:
  websecure:
    address: :443

[...]

providers:
  docker:
    exposedByDefault: false  # Default is true
  file:
    # watch for dynamic configuration changes
    directory: /etc/traefik
    watch: true

As you can see I tried to change the service url to the internal name of the pod but that fails so I must have misunderstood.

Thanks for your help!

Hi @ndy3yju1,
Thanks for sharing me your configuration.

I just saw something wrong in your configuration file.
Before pointing the error, I would explain you one or two basics on Traefik
and how it works.

Traefik comes with 2 kinds of configuration:

  • The first one, called "Static Configuration", contains the parameters required to start and configure Traefik itself. In this configuration, you can find entry points, providers, logging configuration, traefik dashboard and API, etc... This static configuration can be provided in different ways : it could be from command line arguments, or in a file which is by default in /etc/traefik/traefik.yml.
  • The second configuration, called "Dynamic Configuration", is the runtime one. This configuration contains the routing configurations as routers, middlewares, services, ... This dynamic configuration can be provided in different manners as well, but keep it mind that this configuration is hot loaded, and is refreshed based on some orchestrator events (docker, k8s) or at some intervals.

To come back to your problem, I see that your "traefik.yml" file contains both static and dynamic configurations, which are not compatible in the same file.
I can suggest you to move the "http" section of your file into a new one (e.g: /etc/dyn-traefik/blog.yml), and then update the providers.file.directory to /etc/dyn-traefik/blog.yml.

Let me know if it helps you.

Good luck,
Maxence

Hi @moutoum ,
I haven't been ignoring you just trying to implement what I think you said and seem to be failing miserably across the line. I can't even get the dashboard working now so I'm concentrating on that part which I know worked before.

Thank you for your explanation of static and dynamic configuration, somehow the documentation didn't do it for me and I could never grasp the difference.

This is what I did. It still fails but I'm working at it when I have a sec to spare.

The dashboard.yaml is a simlink to /etc/traefik/sites/dashboard.yaml

docker-compose.yaml

---
version: "3.3"

services:

  traefik:
    image: "traefik:v2.7"
    container_name: "traefik"
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    environment:
      SOME_TOKEN: "**********"
    labels:
      - traefik.enable=true
      - traefik.http.routers.traefik.rule=Host(`test.mydomain.com`)
      - traefik.http.routers.traefik.entrypoints=websecure
      - traefik.http.routers.traefik.tls.certresolver=production
      - "traefik.docker.network=traefik_proxy"
    ports:
      - "443:443"
    volumes:
      - /root/docker/traefik/config:/etc/traefik
      - /root/docker/traefik/certs:/certs:ro
      - /root/docker/traefik/logs:/var/logs/traefik
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy

traefik.yaml

---
global:
  checkNewVersion: false
  sendAnonymousUsage: true  # true by default

log:
  level: INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL

accessLog:
  filePath: "/var/log/traefik/access.log"
  format: json

api:
  dashboard: true
  insecure: false

entryPoints:
  websecure:
    address: :443

certificatesResolvers:
  staging:
    acme:
      [redacted]
  production:
    acme:
      [redacted]

tls:
  options:
    default:
      sniStrict: true
      minVersion: VersionTLS12
    require-mtls:
      clientAuth:
        clientAuthType: RequireAndVerifyClientCert
        caFiles:
          - /certs/ca-chain.cert.pem

providers:
  docker:
    exposedByDefault: false
  file:
    directory: /etc/traefik/sites-enabled
    watch: true

pilot:
  token: "**********"

dashboard.yaml

---
http:
  routers:
    dashboard:
      rule: Host(`test.mydomain.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
      service: api@internal
      middlewares:
        - auth

  middlewares:
    auth:
      digestAuth:
        users:
          - "jdoe:traefik:**********"

Does the separation between static and dynamic make more sense now? I am still a bit unsure about the mix of entries as docker-compose labels vs yaml files so I tried to move anything dynamic to the file.

Thanks for your help!