Protecting Development Showcases with Authelia and Caddy
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:
- I needed to add headers to prevent indexing
- 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.ymlusers.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_authmust 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.
3. Cookie domain and login portal mismatch
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
How I Built This Blog: Astro, Docker Images, Caddy and Zero-Touch CI/CD
November 24, 2025
The complete story of building my personal blog with Astro's versatility, Caddy instead of Nginx, Docker containerization, and GitHub Actions for automated deployments.
Azure MCP + EntraAware: Integrating MCP Servers with GitHub Copilot in Visual Studio Code
March 5, 2026
How I combined Azure MCP and EntraAware MCP servers with a custom GitHub Copilot agent in Visual Studio Code to handle Azure auditing tasks in natural language, turning hours of portal navigation into a simple conversation.
Installing Talos Kubernetes on Proxmox: A Complete Guide
February 1, 2026
Learn why Talos is the best OS for running Kubernetes, how it compares to alternatives, and follow a complete guide to setting up a production-grade cluster with Cilium CNI on Proxmox.
Contact Me
Have questions or want to collaborate? Send me a message!