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:

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:

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:

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

However Basic Auth works well only for very simple scenarios:

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:


👤 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:


🔧 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:


⚖️ 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:

With Authelia:


🔒 Security Considerations

If configured correctly this approach is reasonably robust.

Important conditions:

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:

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

In my case it solved two problems at once:


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.

Back to Blog