Protecting Development Showcases with Authelia and Caddy

by Cristian Caiazzo
Authelia Caddy DevOps Security

How I used Authelia and Caddy to protect client development showcases from accidental indexing and unauthorized access — after learning the hard way.

Protecting Development Showcases with Authelia and Caddy

A few days ago I made a mistake. I was developing a website for a client and decided to make it accessible on the internet through a showcase domain so the client could review the progress. Part of the work involved SEO optimization. The problem is that, between accidentally leaving the showcase publicly available longer than intended and the SEO optimization working too well, Google ended up indexing the showcase before the real production website.

I obviously reacted quickly:

  • I shut down the showcase immediately
  • I requested deindexing from Google

But mistakes are useful if you use them to improve your process.

From this situation I came to two conclusions:

  1. I needed to add headers to prevent indexing
  2. I needed to protect my development showcases with authentication

In my setup, all showcases are hosted on the same server and exposed using Caddy. Because of that, I needed a solution that was:

  • centralized
  • integrated with a reverse proxy
  • capable of managing access per user/group
  • relatively simple to configure

After evaluating a few options I decided to use Authelia.

Below is the setup I implemented.


🎯 The Problem

The typical situation looks like this:

flowchart LR
Clients --> Internet
Internet --> ReverseProxy[Caddy]
ReverseProxy --> Showcase1
ReverseProxy --> Showcase2
ReverseProxy --> Showcase3

All showcases are publicly reachable.

This creates several issues:

  • accidental indexing
  • uncontrolled access
  • no way to restrict visibility to specific clients

A simple solution could be Basic Auth at the reverse proxy level.

However Basic Auth works well only for very simple scenarios:

  • a single password
  • few services
  • no distinction between users

As soon as you have multiple clients and multiple environments, Basic Auth becomes difficult to manage.

With Authelia instead you can define access policies per domain and per user group.


🏗️ Architecture

The architecture becomes the following:

flowchart TD

User[Client Browser]
Proxy[Caddy Reverse Proxy]
Auth[Authelia]
App1[Client A Showcase]
App2[Client B Showcase]

User --> Proxy
Proxy -->|verify session| Auth
Auth -->|OK| Proxy
Proxy --> App1
Proxy --> App2

Caddy receives the request and asks Authelia:

Is this user authorized to access the resource?

If the answer is yes, the request is forwarded to the showcase.

If not, the user is redirected to the login page.


🔐 Authentication Flow

sequenceDiagram

participant User
participant Caddy
participant Authelia
participant Showcase

User->>Caddy: Request showcase
Caddy->>Authelia: Verify session

alt Not authenticated
Authelia-->>Caddy: Redirect to login
Caddy-->>User: Redirect login portal
User->>Authelia: Authenticate
Authelia-->>User: Session cookie
end

User->>Caddy: New request
Caddy->>Authelia: Verify cookie
Authelia-->>Caddy: OK

Caddy->>Showcase: Forward request
Showcase-->>User: Response

🐳 Installing Authelia with Docker

Authelia runs in a dedicated Docker container.

Minimal example:

services:

  authelia:
    image: authelia/authelia:4.39
    container_name: authelia

    volumes:
      - ./configuration.yml:/config/configuration.yml:ro
      - ./users.yml:/config/users.yml:ro
      - ./data:/config/data

    restart: unless-stopped

Key files:

  • configuration.yml
  • users.yml

👤 Defining Users

Users can be defined in a YAML file.

Example:

users:
  client1:
    displayname: "Client 1"
    password: "<argon2_hash>"
    email: "client@example.com"
    groups:
      - showcase_client1

Passwords are stored as Argon2 hashes.

To generate the hash:

docker run --rm authelia/authelia authelia crypto hash generate argon2 --password 'password'

🛡️ Authelia Access Policies

Example configuration:

access_control:
  default_policy: deny

  rules:

    - domain: showcase-client1.example.com
      policy: one_factor
      subject:
        - "group:showcase_client1"

    - domain: showcase-client2.example.com
      policy: one_factor
      subject:
        - "group:showcase_client2"

Behavior:

  • everything is denied by default
  • each domain has a rule
  • only users in the correct group can access it

🔧 Integrating with Caddy

Authelia is exposed through Caddy:

auth.example.com {
 reverse_proxy authelia:9091
}

Then forward authentication is used to protect services.

Reusable snippet:

(authelia_protect) {
 forward_auth authelia:9091 {
  uri /api/verify?rd=https://auth.example.com/
  copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
 }
}

Applying it to a showcase:

showcase-client1.example.com {

 import authelia_protect

 reverse_proxy showcase1:80

 header {
  X-Robots-Tag "noindex, nofollow"
 }
}

This ensures:

  • the showcase requires authentication
  • search engines will not index it

⚖️ Comparison with Basic Auth

flowchart LR

subgraph BasicAuth
User1 --> Proxy1
Proxy1 --> AppA
Proxy1 --> AppB
end

subgraph Authelia
User2 --> Proxy2
Proxy2 --> Auth
Auth --> Proxy2
Proxy2 --> AppA2
Proxy2 --> AppB2
end

With Basic Auth:

  • anyone with the password can access everything

With Authelia:

  • access is controlled per user and per group

🔒 Security Considerations

If configured correctly this approach is reasonably robust.

Important conditions:

  • backend services must not be exposed directly
  • all traffic must go through the reverse proxy
  • forward_auth must be applied to protected routes
  • Authelia should use default_policy: deny

Under these conditions it is not possible to access protected showcases without passing authentication.


⚠️ Common Mistakes When Configuring Authelia

1. Using the display name instead of the username

In users.yml the actual username is the YAML key:

users:
  client1:
    displayname: "Client 1"

The login must use:

username: client1

not the display name.

2. Generating the password hash incorrectly

Passwords must be hashed with Argon2.

Correct method:

docker run --rm authelia/authelia authelia crypto hash generate argon2 --password 'password'

A common mistake is generating hashes using commands that introduce trailing newline characters, causing login failures.

Authelia requires the login portal to share the same cookie scope as the protected domain.

Incorrect configuration:

session:
  cookies:
    - domain: example-app.com
      authelia_url: https://auth.example.com

Correct configuration:

session:
  cookies:
    - domain: example-app.com
      authelia_url: https://auth.example-app.com

If this rule is violated Authelia will refuse to start.

4. Accidentally exposing backend services

A dangerous mistake is publishing container ports directly.

Example:

ports:
  - "3000:3000"

This allows direct access to the application, bypassing the reverse proxy and Authelia.

Instead, containers should communicate only through the internal Docker network.

5. Protecting only some routes

When configuring the reverse proxy it’s easy to protect only part of the application.

Example:

handle / {
  import authelia_protect
}

but forgetting something like:

handle /api/*

which would leave the API unprotected.

6. Preventing accidental indexing

Even when authentication is enabled, it is still good practice to prevent indexing.

With Caddy this can be done with:

header {
  X-Robots-Tag "noindex, nofollow"
}

✅ Conclusion

This setup is not the simplest possible solution.

If you only have one service and a few users, Basic Auth may be enough.

However, when you start managing:

  • multiple clients
  • multiple showcases
  • separated access permissions

a centralized authentication system such as Authelia becomes much easier to manage.

In my case it solved two problems at once:

  • preventing accidental indexing of development showcases
  • allowing clients to access only their own environments

One mistake turned into a proper security improvement — that’s the best outcome you can hope for. If you’re running client showcases, don’t wait for a similar incident before locking them down. Feel free to reach out if you have questions or want to share how you solved the same problem!

Related Articles

Contact Me

Have questions or want to collaborate? Send me a message!

I accept the Privacy Policy and authorize the use of my data for handling this request.