Overview
Many modern applications support OpenID Connect natively.
Plenty of others do not.
You may have an application that:
- supports only trusted HTTP headers;
- has no SAML implementation;
- has no enterprise SSO support at all;
- cannot be modified;
- runs inside a container;
- is already deployed behind Apache HTTP Server.
In that situation, Apache can act as the authentication layer in front of the application.
The architecture looks like:
Browser
|
| HTTPS
v
Apache HTTP Server
+ mod_auth_mellon
|
| SAML 2.0
+---------------------> Identity Provider
|
| authenticated request
v
Backend Application
Apache and mod_auth_mellon act as the SAML Service Provider, or SP.
The upstream identity system acts as the Identity Provider, or IdP.
The backend application does not need to understand SAML at all.
Instead, Apache can:
- intercept unauthenticated requests;
- redirect the browser to the IdP;
- receive and validate the SAML response;
- establish a Mellon session;
- expose selected SAML attributes;
- reverse proxy the authenticated request to the application.
This is especially useful for legacy applications or containerized applications that already trust authentication headers from a reverse proxy.
This guide covers both:
- a traditional Apache installation;
- a Docker deployment where Apache and
mod_auth_mellonrun as a dedicated SAML reverse-proxy container.
The examples use generic hostnames and identities throughout.
What is mod_auth_mellon?
mod_auth_mellon is an Apache HTTP Server authentication module that implements a SAML 2.0 Service Provider.
The actively maintained project is the Latchset mod_auth_mellon project.
Once loaded into Apache, Mellon can protect URLs using directives such as:
MellonEnable auth
and communicate with a SAML Identity Provider using metadata.
A typical Mellon deployment requires four important artifacts:
SP metadata
SP private key
SP certificate
IdP metadata
plus Apache configuration.
Conceptually:
SAML metadata exchange
+--------------------------------------+
| |
v |
+-------------+ +-------------+
| Apache / SP | | IdP |
| Mellon | | |
+-------------+ +-------------+
|
v
Web Application
Real-world scenario
Assume you have an internal application running at:
http://application:8080
inside Docker.
The application itself has no SAML implementation.
You want users to access it at:
https://portal.example.test
and authenticate with the organization’s SAML Identity Provider.
The desired architecture is:
SAML
+---------+
| |
v |
Browser ---> Apache Mellon ---> Identity Provider
|
|
v
Backend Application
:8080
Only Apache should be reachable by end users.
The application container should remain accessible only on the internal Docker network.
That detail is important because the backend may trust Apache-provided identity headers.
Why put SAML in the reverse proxy?
Suppose the application understands this:
X-Authenticated-User: user@example.test
but has no idea what this is:
<samlp:Response>
Apache becomes the translation boundary.
SAML assertion
↓
mod_auth_mellon
↓
Apache environment variables
↓
trusted HTTP headers
↓
application identity
This lets you add SAML authentication without modifying the application.
It also keeps the application’s authentication integration small.
The application only needs to trust requests arriving from Apache.
Security warning about trusted headers
This architecture is secure only when users cannot bypass Apache.
If the backend trusts:
X-Authenticated-User
then an attacker who can connect directly to the backend might simply send:
X-Authenticated-User: administrator
and impersonate another user.
The required network architecture is therefore:
Internet / Users
|
v
Apache + Mellon
|
private network
|
v
Application
Not:
+------ Apache
|
Users -------+
|
+------ Application
The application should normally have no published host port.
Step 1: Install Apache and mod_auth_mellon
RHEL-style systems
Install:
sudo dnf install \
httpd \
mod_auth_mellon \
mod_ssl \
openssl
Package names may vary slightly depending on distribution and enabled repositories.
Debian or Ubuntu
Install:
sudo apt update
sudo apt install \
apache2 \
libapache2-mod-auth-mellon \
openssl \
ca-certificates
Enable the required Apache modules:
sudo a2enmod \
auth_mellon \
ssl \
proxy \
proxy_http \
headers
If the backend uses WebSockets:
sudo a2enmod proxy_wstunnel
Step 2: Create the Mellon configuration directory
On Debian-style systems:
sudo mkdir -p /etc/apache2/mellon
On RHEL-style systems you might instead use:
sudo mkdir -p /etc/httpd/saml2
For the rest of this guide:
/etc/apache2/mellon
will be used.
The directory will eventually contain:
/etc/apache2/mellon/
├── sp-key.pem
├── sp-cert.pem
├── sp-metadata.xml
└── idp-metadata.xml
Protect the private key:
sudo chmod 600 /etc/apache2/mellon/sp-key.pem
Step 3: Choose the SAML SP Entity ID
Every SAML entity needs a unique identifier.
For the Service Provider, use something stable such as:
https://portal.example.test/mellon/metadata
This is the Entity ID.
Do not confuse the Entity ID with the Assertion Consumer Service URL.
A typical Mellon configuration may use:
Entity ID:
https://portal.example.test/mellon/metadata
while the ACS becomes:
https://portal.example.test/mellon/postResponse
The exact endpoint paths are generated from the Mellon endpoint path.
Step 4: Generate the Mellon SP metadata and key pair
mod_auth_mellon packages normally include a metadata-generation utility.
Depending on the distribution, the executable may be named:
mellon_create_metadata
or:
mellon_create_metadata.sh
Check:
command -v mellon_create_metadata
command -v mellon_create_metadata.sh
Generate metadata using:
mellon_create_metadata \
'https://portal.example.test/mellon/metadata' \
'https://portal.example.test/mellon'
The tool creates three files:
private key
certificate
SP metadata
The generated filenames depend on the Entity ID.
Normalize them:
mv *.key sp-key.pem
mv *.cert sp-cert.pem
mv *.xml sp-metadata.xml
Then protect the key:
chmod 600 sp-key.pem
A metadata generator is preferable to manually writing the XML because it creates Mellon-compatible SAML endpoints and embeds the SP certificate correctly.
Step 5: Inspect the generated SP metadata
Open:
less sp-metadata.xml
You should find an EntityDescriptor resembling:
<EntityDescriptor
entityID="https://portal.example.test/mellon/metadata">
<SPSSODescriptor>
...
<AssertionConsumerService
Location="https://portal.example.test/mellon/postResponse" />
</SPSSODescriptor>
</EntityDescriptor>
You will also see the public certificate represented as Base64 inside:
<ds:X509Certificate>
The private key is not included in the metadata.
That distinction is important:
SP metadata
↓
safe to provide to the IdP
SP private key
↓
must remain on the SP
Step 6: Register the SP with the Identity Provider
Provide:
sp-metadata.xml
to the SAML Identity Provider administrator.
The IdP uses the metadata to learn:
- the SP Entity ID;
- Assertion Consumer Service URLs;
- Single Logout endpoints;
- supported bindings;
- the SP public certificate.
The IdP registration must also define which attributes are released.
For example:
mail
displayName
groups
The exact attribute names depend on the Identity Provider.
Step 7: Obtain the IdP metadata
The Identity Provider should publish metadata describing:
- IdP Entity ID;
- Single Sign-On services;
- Single Logout services;
- signing certificate;
- supported bindings.
Download it:
curl \
-fL \
'https://idp.example.test/saml/metadata' \
-o idp-metadata.xml
Do not use:
curl -k
in production merely to bypass TLS validation.
Instead, make sure the server running Mellon trusts the CA that issued the IdP HTTPS certificate.
Inspect the metadata:
less idp-metadata.xml
Look for:
<IDPSSODescriptor>
and one or more:
<SingleSignOnService>
entries.
Step 8: Create the basic Apache Mellon configuration
A minimal configuration resembles:
<Location />
AuthType Mellon
MellonEnable auth
MellonEndpointPath /mellon
MellonSPMetadataFile /etc/apache2/mellon/sp-metadata.xml
MellonSPPrivateKeyFile /etc/apache2/mellon/sp-key.pem
MellonSPCertFile /etc/apache2/mellon/sp-cert.pem
MellonIdPMetadataFile /etc/apache2/mellon/idp-metadata.xml
Require valid-user
</Location>
The critical directives are:
MellonSPMetadataFile
MellonSPPrivateKeyFile
MellonSPCertFile
MellonIdPMetadataFile
Together they establish the trust relationship between the SP and IdP.
MellonEnable auth versus info
Mellon supports different operating modes.
For protected locations:
MellonEnable auth
means authentication is required.
A configuration using:
MellonEnable info
can expose Mellon session information without forcing authentication.
A common pattern is:
<Location />
MellonEnable info
MellonEndpointPath /mellon
MellonSPMetadataFile ...
MellonSPPrivateKeyFile ...
MellonSPCertFile ...
MellonIdPMetadataFile ...
</Location>
<Location /secure>
AuthType Mellon
MellonEnable auth
Require valid-user
</Location>
This allows selected routes to require authentication.
If the entire application requires SSO, protecting / is simpler.
Step 9: Configure the Mellon endpoint
Use:
MellonEndpointPath /mellon
Mellon creates internal SAML endpoints beneath this path.
Typical URLs include:
/mellon/metadata
/mellon/login
/mellon/logout
/mellon/postResponse
Do not reverse proxy these URLs to the backend application.
Apache itself must handle them.
If Apache also acts as the reverse proxy:
ProxyPass /mellon !
The exclamation mark tells Apache:
Do not proxy this path.
This is extremely important.
Otherwise:
/mellon/postResponse
might be forwarded to the application instead of processed by Mellon.
Step 10: Configure SAML session cookies
A practical configuration is:
MellonSecureCookie On
MellonCookieSameSite Lax
or, depending on the SAML flow and IdP behavior:
MellonCookieSameSite None
If you use:
MellonCookieSameSite None
also use:
MellonSecureCookie On
so the cookie is sent only over HTTPS.
Test the actual authentication flow in current browsers rather than choosing a SameSite value blindly.
Step 11: Select the authenticated username
Mellon makes the SAML NameID available as:
NAME_ID
The default username source is commonly:
MellonUser "NAME_ID"
Explicitly configure it:
MellonUser "NAME_ID"
After authentication, Apache can expose:
MELLON_NAME_ID
for use by modules and applications.
If the IdP sends an email-style NameID:
user@example.test
that can become the authenticated application identity.
Alternatively, you can select another SAML attribute.
For example:
MellonUser "uid"
The IdP must actually release that attribute.
Step 12: Require an attribute
You can require conditions beyond merely:
Require valid-user
Mellon provides authorization directives such as:
MellonCond
For example:
MellonCond "department" "^Engineering$" [REG]
or:
MellonCond "NAME_ID" ".+" [REG]
This lets Apache reject a SAML-authenticated user who does not satisfy required attributes.
Keep authorization rules simple unless Apache is genuinely intended to be the authorization layer.
In many architectures:
Mellon
↓
Authentication
Application
↓
Authorization
is easier to maintain.
Step 13: Map SAML attributes to Apache environment variables
Assume the IdP releases:
mail
displayName
memberOf
Map them with:
MellonSetEnv "email" "mail"
MellonSetEnv "displayName" "displayName"
MellonSetEnv "groups" "memberOf"
By default Mellon prefixes generated environment variables with:
MELLON_
so these become approximately:
MELLON_email
MELLON_displayName
MELLON_groups
The maintained Mellon configuration examples document MellonSetEnv specifically for mapping incoming IdP attributes to local Apache environment-variable names.
Step 14: Understand multi-valued SAML attributes
This becomes important with group membership.
Suppose the IdP returns:
groups:
app-users
app-editors
app-admins
Without attribute merging, Mellon may expose these as indexed environment variables such as:
MELLON_groups
MELLON_groups_0
MELLON_groups_1
MELLON_groups_2
The Mellon documentation notes that:
MellonMergeEnvVars
can combine multi-valued attributes into one variable.
For example:
MellonMergeEnvVars On ";"
could produce:
MELLON_groups=app-users;app-editors;app-admins
The default merge separator is a semicolon; Mellon permits specifying another separator.
Choose the delimiter according to what the backend expects.
Avoid delimiters that occur inside group values
Suppose directory memberships arrive as full distinguished names:
CN=app-users,OU=Groups,DC=example,DC=test
CN=app-admins,OU=Groups,DC=example,DC=test
Using:
MellonMergeEnvVars On ","
would create:
CN=app-users,OU=Groups,DC=example,DC=test,CN=app-admins,...
Now it is impossible for a downstream application to distinguish:
group separator commas
from:
LDAP DN commas
Prefer either:
- having the IdP release simplified application-specific group names; or
- using a delimiter that cannot appear in the source values.
The better architecture is often:
Directory groups
↓
IdP filters/transforms
↓
app-users
app-admins
↓
SAML
↓
Mellon
rather than sending every raw LDAP DN to the application.
Step 15: Reverse proxy the application
Enable proxying:
ProxyPreserveHost On
ProxyPass /mellon !
ProxyPass / http://application:8080/
ProxyPassReverse / http://application:8080/
The order matters conceptually:
/mellon
↓
handled by Apache
everything else
↓
proxied to backend
Apache’s mod_proxy acts as the reverse proxy and can preserve the original host with ProxyPreserveHost.
Step 16: Pass authenticated identity to the backend
Suppose the application trusts:
X-Authenticated-User
X-Authenticated-Email
X-Authenticated-Groups
Configure:
RequestHeader set X-Authenticated-User \
"%{MELLON_NAME_ID}e" \
"expr=-n %{env:MELLON_NAME_ID}"
RequestHeader set X-Authenticated-Email \
"%{MELLON_email}e" \
"expr=-n %{env:MELLON_email}"
RequestHeader set X-Authenticated-Groups \
"%{MELLON_groups}e" \
"expr=-n %{env:MELLON_groups}"
The:
e
means the value comes from an Apache environment variable.
This translates:
SAML NameID
↓
MELLON_NAME_ID
↓
X-Authenticated-User
for the backend.
Step 17: Strip incoming authentication headers first
Never simply do this:
RequestHeader set X-Authenticated-User "%{MELLON_NAME_ID}e"
without considering client input.
A malicious request might arrive containing:
X-Authenticated-User: administrator
Strip the trusted identity headers first:
RequestHeader unset X-Authenticated-User
RequestHeader unset X-Authenticated-Email
RequestHeader unset X-Authenticated-Groups
Then set them from Mellon:
RequestHeader set X-Authenticated-User \
"%{MELLON_NAME_ID}e" \
"expr=-n %{env:MELLON_NAME_ID}"
The trust boundary becomes:
Client header
↓
removed by Apache
SAML identity
↓
Mellon environment
↓
new trusted header
This is one of the most important security controls when putting SAML in front of a trusted-header application.
Step 18: Complete traditional Apache virtual host
A simplified complete configuration might look like:
<VirtualHost *:443>
ServerName portal.example.test
SSLEngine On
SSLCertificateFile /etc/apache2/tls/server.crt
SSLCertificateKeyFile /etc/apache2/tls/server.key
SSLProtocol -all +TLSv1.2 +TLSv1.3
ErrorLog ${APACHE_LOG_DIR}/portal-error.log
CustomLog ${APACHE_LOG_DIR}/portal-access.log combined
<Location />
AuthType Mellon
MellonEnable auth
MellonEndpointPath /mellon
MellonSPMetadataFile \
/etc/apache2/mellon/sp-metadata.xml
MellonSPPrivateKeyFile \
/etc/apache2/mellon/sp-key.pem
MellonSPCertFile \
/etc/apache2/mellon/sp-cert.pem
MellonIdPMetadataFile \
/etc/apache2/mellon/idp-metadata.xml
MellonSecureCookie On
MellonCookieSameSite Lax
MellonUser "NAME_ID"
MellonSetEnv "email" "mail"
MellonSetEnv "displayName" "displayName"
MellonSetEnv "groups" "groups"
MellonMergeEnvVars On ";"
Require valid-user
</Location>
ProxyPass /mellon !
RequestHeader unset X-Authenticated-User
RequestHeader unset X-Authenticated-Email
RequestHeader unset X-Authenticated-Groups
RequestHeader set X-Authenticated-User \
"%{MELLON_NAME_ID}e" \
"expr=-n %{env:MELLON_NAME_ID}"
RequestHeader set X-Authenticated-Email \
"%{MELLON_email}e" \
"expr=-n %{env:MELLON_email}"
RequestHeader set X-Authenticated-Groups \
"%{MELLON_groups}e" \
"expr=-n %{env:MELLON_groups}"
ProxyPreserveHost On
ProxyPass / http://application:8080/
ProxyPassReverse / http://application:8080/
</VirtualHost>
Adjust attribute names to whatever your IdP actually releases.
Step 19: Validate the Apache configuration
Always run:
apachectl configtest
or:
apachectl -t
Expected:
Syntax OK
Then restart:
sudo systemctl restart apache2
or on RHEL:
sudo systemctl restart httpd
Check:
sudo systemctl status apache2
and review the error log if authentication fails.
Step 20: Verify Mellon metadata from the browser
Mellon normally exposes the SP metadata endpoint beneath:
/mellon/metadata
For example:
https://portal.example.test/mellon/metadata
Retrieve it:
curl \
-fsS \
https://portal.example.test/mellon/metadata
Confirm the:
entityID
and endpoints match what was registered with the IdP.
A mismatch between the runtime metadata and the registered metadata is a common cause of SAML failures.
Docker architecture with Apache Mellon
Now consider the same design in Docker.
Instead of installing Apache directly on the host:
SAML
+-----------+
| |
v |
Browser ---> Mellon Container ---> Identity Provider
|
| Docker network
|
v
Application Container
The Apache container handles:
TLS
SAML
trusted headers
reverse proxy
while the application container handles only the application.
This separation is particularly useful when the application has no native SAML support.
Why use a separate Mellon container?
A dedicated authentication proxy allows the application image to remain untouched.
Instead of:
Application image
+ Apache
+ SAML libraries
+ Mellon
+ certificates
+ metadata
you get:
Application image
↓
application only
and:
SAML proxy image
↓
Apache
mod_auth_mellon
TLS
SP configuration
This follows the container principle of separating distinct responsibilities.
Step 21: Create the Docker directory structure
Create:
saml-proxy/
├── Dockerfile
├── apache.conf
├── entrypoint.sh
├── mellon/
│ ├── sp-metadata.xml
│ ├── sp-cert.pem
│ ├── sp-key.pem
│ └── idp-metadata.xml
└── tls/
├── server.crt
└── server.key
Do not commit:
sp-key.pem
server.key
to source control.
A .gitignore should include:
saml-proxy/mellon/sp-key.pem
saml-proxy/tls/server.key
You may choose to ignore the entire generated key directory.
Step 22: Create the Dockerfile
A Debian-based example:
FROM debian:13-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
apache2 \
libapache2-mod-auth-mellon \
ca-certificates \
curl \
openssl \
&& a2enmod \
auth_mellon \
ssl \
proxy \
proxy_http \
proxy_wstunnel \
headers \
&& a2dissite 000-default \
&& rm -rf /var/lib/apt/lists/*
COPY apache.conf \
/etc/apache2/sites-available/application.conf
COPY entrypoint.sh \
/usr/local/bin/entrypoint.sh
RUN a2ensite application \
&& chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 8443
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
This installs only the components required for:
Apache
Mellon
TLS
proxying
metadata retrieval
The exact base-image and package versions should be pinned according to your normal container lifecycle policy.
Step 23: Create the Docker Apache configuration
Use:
Listen 8443
<VirtualHost *:8443>
ServerName portal.example.test
SSLEngine On
SSLCertificateFile \
/etc/apache2/tls/server.crt
SSLCertificateKeyFile \
/etc/apache2/tls/server.key
SSLProtocol -all +TLSv1.2 +TLSv1.3
ErrorLog /dev/stderr
CustomLog /dev/stdout combined
<Location />
AuthType Mellon
MellonEnable auth
MellonEndpointPath /mellon
MellonSPMetadataFile \
/etc/apache2/mellon/sp-metadata.xml
MellonSPPrivateKeyFile \
/etc/apache2/mellon/sp-key.pem
MellonSPCertFile \
/etc/apache2/mellon/sp-cert.pem
MellonIdPMetadataFile \
/etc/apache2/mellon/idp-metadata.xml
MellonSecureCookie On
MellonCookieSameSite Lax
MellonUser "NAME_ID"
MellonSetEnv "email" "mail"
MellonSetEnv "groups" "groups"
MellonMergeEnvVars On ";"
Require valid-user
</Location>
ProxyPass /mellon !
RequestHeader unset X-Authenticated-User
RequestHeader unset X-Authenticated-Email
RequestHeader unset X-Authenticated-Groups
RequestHeader set X-Authenticated-User \
"%{MELLON_NAME_ID}e" \
"expr=-n %{env:MELLON_NAME_ID}"
RequestHeader set X-Authenticated-Email \
"%{MELLON_email}e" \
"expr=-n %{env:MELLON_email}"
RequestHeader set X-Authenticated-Groups \
"%{MELLON_groups}e" \
"expr=-n %{env:MELLON_groups}"
ProxyPreserveHost On
ProxyPass \
/ \
http://application:8080/ \
nocanon \
timeout=3600
ProxyPassReverse \
/ \
http://application:8080/
</VirtualHost>
The Docker pattern is the same as traditional Apache.
The difference is that:
application
is the Docker DNS name of the backend service.
Step 24: Create the Docker entrypoint
The entrypoint can validate permissions and then start Apache:
#!/usr/bin/env bash
set -euo pipefail
MELLON_DIR="/etc/apache2/mellon"
required_files=(
"$MELLON_DIR/sp-key.pem"
"$MELLON_DIR/sp-cert.pem"
"$MELLON_DIR/sp-metadata.xml"
"$MELLON_DIR/idp-metadata.xml"
)
for file in "${required_files[@]}"; do
if [[ ! -s "$file" ]]; then
echo "ERROR: Missing required file: $file" >&2
exit 1
fi
done
chmod 600 "$MELLON_DIR/sp-key.pem"
apache2ctl configtest
exec apache2ctl -D FOREGROUND
Using:
exec
makes Apache the container’s main process so it receives container stop signals correctly.
Step 25: Create the Docker Compose stack
A simple Compose file:
services:
application:
image: example/application:latest
restart: unless-stopped
networks:
- app_network
# Intentionally no "ports:" section.
#
# Users must not be able to bypass
# the SAML reverse proxy.
saml-proxy:
build:
context: ./saml-proxy
restart: unless-stopped
depends_on:
- application
ports:
- "443:8443"
volumes:
- ./saml-proxy/mellon:/etc/apache2/mellon:ro
- ./saml-proxy/tls:/etc/apache2/tls:ro
networks:
- app_network
networks:
app_network:
Notice the backend application has:
no host port
That is intentional.
Docker networking allows:
saml-proxy
to reach:
application:8080
without exposing port 8080 to users.
Step 26: Build and start the containers
Run:
docker compose build
Then:
docker compose up -d
Check:
docker compose ps
Inspect Apache logs:
docker compose logs -f saml-proxy
The proxy should listen externally while the application remains internal to the Compose network.
Step 27: Generate Mellon metadata using the container image
You do not need Mellon installed on the Docker host.
Use the image itself.
For example:
docker compose build saml-proxy
Then run the metadata utility in a temporary container.
Conceptually:
docker run \
--rm \
-v "$PWD/saml-proxy/mellon:/out" \
-w /out \
--entrypoint mellon_create_metadata \
saml-proxy-image \
'https://portal.example.test/mellon/metadata' \
'https://portal.example.test/mellon'
Because Docker Compose image names vary by project, another useful pattern is:
docker compose run \
--rm \
--entrypoint mellon_create_metadata \
saml-proxy \
'https://portal.example.test/mellon/metadata' \
'https://portal.example.test/mellon'
Afterward, normalize the generated names:
cd saml-proxy/mellon
mv *.key sp-key.pem
mv *.cert sp-cert.pem
mv *.xml sp-metadata.xml
chmod 600 sp-key.pem
This makes the metadata-generation process reproducible without installing packages on the host.
Step 28: Optionally download IdP metadata at container startup
Instead of storing:
idp-metadata.xml
inside the image, the container can retrieve it at startup.
Example:
IDP_METADATA_URL="${SAML_IDP_METADATA_URL:?Required}"
for attempt in $(seq 1 20); do
if curl \
-fsS \
-o /etc/apache2/mellon/idp-metadata.xml \
"$IDP_METADATA_URL"
then
break
fi
echo "Waiting for IdP metadata..."
sleep 3
done
if [[ ! -s /etc/apache2/mellon/idp-metadata.xml ]]; then
echo "Unable to retrieve IdP metadata" >&2
exit 1
fi
This can be useful in disposable development environments.
For production, decide deliberately whether metadata should be:
version-controlled configuration
or:
retrieved dynamically
Dynamic metadata retrieval has tradeoffs.
An IdP metadata change could alter trusted signing certificates or endpoints without a normal application deployment.
Step 29: Development versus production SP keys
For a disposable local environment, you might generate a new SP key each time:
container starts
↓
generate throwaway key
↓
generate metadata
That can work when the test IdP is generated alongside it.
Do not use that pattern when the SP is registered with an external production IdP.
Production requires stable SP identity material:
sp-key.pem
sp-cert.pem
sp-metadata.xml
If the key changes unexpectedly, the IdP registration may no longer match the SP.
Use:
persistent secret
Docker secret
Kubernetes secret
vault
deployment pipeline
or another managed secret mechanism for the private key.
Step 30: Local SAML development with a disposable IdP
A fully self-contained development stack can include a test IdP:
Browser
|
v
Apache Mellon
|
| SAML
v
Disposable Local IdP
|
v
Test Account
The Compose stack becomes:
application
saml-proxy
local-idp
This has a useful property:
Developers can test the complete SAML flow without depending on an organization’s real identity environment.
The local IdP can release generic attributes such as:
mail
displayName
groups
Example test identity:
user@example.test
displayName:
Example User
groups:
app-users
app-editors
Never copy real enterprise identities, domains, directory DNs, or group memberships into the test IdP.
Step 31: Development Compose example
Conceptually:
services:
application:
image: example/application:latest
networks:
- app_network
saml-proxy:
build:
context: ./saml-proxy
depends_on:
- application
- local-idp
environment:
SAML_IDP_METADATA_URL: >
http://local-idp:8080/saml/metadata
volumes:
- ./saml-proxy/mellon:/etc/apache2/mellon
- ./saml-proxy/tls:/etc/apache2/tls
ports:
- "443:8443"
networks:
- app_network
local-idp:
image: example/test-saml-idp:latest
ports:
- "127.0.0.1:8081:8080"
networks:
- app_network
networks:
app_network:
Notice:
127.0.0.1
for the local IdP’s host binding.
That prevents a developer-only identity provider from accidentally being exposed to the network.
Step 32: TLS inside the Docker SAML proxy
The Mellon SP should normally be externally reachable over HTTPS:
https://portal.example.test
SAML metadata must therefore describe HTTPS endpoints.
For local testing, generate a temporary self-signed certificate:
openssl req \
-x509 \
-newkey rsa:2048 \
-nodes \
-keyout server.key \
-out server.crt \
-days 30 \
-subj '/CN=portal.example.test' \
-addext 'subjectAltName=DNS:portal.example.test'
For production, use a certificate issued by an appropriate CA.
For more examples, see OpenSSL Self Signed Certificate.
You can inspect a certificate with:
openssl x509 \
-in server.crt \
-text \
-noout
See OpenSSL View Certificate for additional inspection commands.
Step 33: TLS termination in front of Mellon
Some architectures have another load balancer:
Browser
|
HTTPS
|
v
Load Balancer
|
HTTPS
|
v
Apache Mellon
|
HTTP
|
v
Application
This is usually easier than:
Browser
|
HTTPS
|
v
Load Balancer
|
HTTP
|
v
Apache Mellon
because Mellon constructs SAML endpoint URLs based on how Apache sees the request.
Historically, running Mellon behind an HTTPS-offloading proxy while Mellon itself receives HTTP has caused SAML destination mismatches if the SP expects http:// while the IdP posts to an externally advertised https:// URL.
If practical, re-encrypt from the load balancer to Apache so Mellon itself sees HTTPS.
Step 34: UseCanonicalName and ServerName
When Apache sits behind a reverse proxy or load balancer, stable URL construction is important.
A configuration may include:
ServerName portal.example.test
UseCanonicalName On
This helps Apache use the intended canonical hostname.
The Mellon metadata, Apache ServerName, public DNS, and load-balancer hostname should all describe the same externally visible service.
Avoid a situation like:
metadata:
https://portal.example.test/mellon/postResponse
Apache thinks:
https://container-7c4f:8443/mellon/postResponse
SAML is sensitive to exact endpoint URLs.
Step 35: WebSocket applications
Many modern web applications use WebSockets.
If the application requires them, enable:
a2enmod proxy_wstunnel
Modern Apache versions can handle WebSocket upgrades through mod_proxy_http as well, depending on configuration.
A common proxy configuration is:
ProxyPass \
/ \
http://application:8080/ \
nocanon \
upgrade=websocket \
timeout=3600
Test normal HTTP requests and WebSockets separately after introducing the SAML proxy.
Step 36: Log authenticated identities safely
During troubleshooting, temporarily log Mellon’s authenticated user:
LogFormat \
"%h %l %{MELLON_NAME_ID}e %t \"%r\" %>s %b" \
saml_combined
CustomLog /dev/stdout saml_combined
This can help confirm:
authentication succeeded
↓
Mellon session exists
↓
NAME_ID is populated
Do not log:
full SAML assertions
authentication secrets
sensitive attributes
in production unless there is a specific diagnostic requirement.
Step 37: Debug Mellon attributes
For temporary troubleshooting, create a protected diagnostic endpoint or increase Apache logging.
The important thing to determine is what Mellon actually receives.
For example:
MELLON_NAME_ID=user@example.test
MELLON_email=user@example.test
MELLON_groups=app-users;app-editors
If the expected value is missing, troubleshoot in this order:
1. Did the IdP release the attribute?
2. Is the attribute name correct?
3. Did Mellon receive it?
4. Did MellonSetEnv map it?
5. Did Apache set the backend header?
6. Did the application read the header?
Avoid changing all six layers simultaneously.
Step 38: Troubleshoot endless SAML redirect loops
Symptoms:
Application
↓
IdP
↓
Application
↓
IdP
↓
...
Possible causes include:
- Mellon session cookie not being retained;
- SameSite cookie behavior;
- HTTP/HTTPS mismatch;
- incorrect SP metadata;
- load balancer rewriting hostnames;
- multiple application nodes with inconsistent configuration.
Check:
MellonSecureCookie On
and the selected:
MellonCookieSameSite
value.
Inspect the browser’s cookies and network requests.
Step 39: Troubleshoot invalid Destination
You may see an error resembling:
Invalid Destination on Response
This commonly means:
IdP sent response to:
https://portal.example.test/mellon/postResponse
while Mellon expected something different.
Check:
SP metadata ACS URL
Apache ServerName
HTTP versus HTTPS
load-balancer forwarding
MellonEndpointPath
The URLs must agree.
SAML endpoint mismatches are exact-string problems surprisingly often.
Step 40: Troubleshoot missing attributes
Suppose authentication succeeds, but:
MELLON_email
is empty.
First inspect the SAML response using a browser SAML tracing extension.
Look for:
<saml:Attribute Name="mail">
If the IdP does not send the attribute, Apache cannot map it.
If it does exist, verify:
MellonSetEnv "email" "mail"
Remember that SAML attribute names are case-sensitive in many integrations.
Step 41: Troubleshoot only the first group appearing
This is a common Mellon issue with multi-valued attributes.
If:
groups
contains multiple values but:
MELLON_groups
contains only one, check:
MellonMergeEnvVars
Mellon’s documented default is:
Off
while indexed variables may hold additional values.
Enable merging:
MellonMergeEnvVars On ";"
Then verify:
MELLON_groups=app-users;app-editors;app-admins
Choose the separator to match the application.
Step 42: Troubleshoot 403 after successful SAML login
A SAML response may validate successfully but Apache can still deny authorization.
Check:
Require valid-user
and any:
MellonCond
MellonRequire
directives.
For example:
MellonCond "groups" "^app-users$" [REG]
may not behave as intended if:
groups
is multi-valued or has a different format.
Authentication success does not imply authorization success.
Step 43: Troubleshoot backend header spoofing
If users can connect to:
http://application:8080
directly, stop troubleshooting SAML and fix the network architecture first.
The backend should not trust headers from arbitrary clients.
In Docker Compose, remove:
ports:
- "8080:8080"
from the application.
Use only:
networks:
- app_network
so Apache can reach it internally.
Step 44: Health checking Mellon
A useful health check is the metadata endpoint:
curl \
-fsSk \
https://localhost:8443/mellon/metadata \
> /dev/null
In Compose:
healthcheck:
test:
[
"CMD-SHELL",
"curl -fsSk -o /dev/null https://localhost:8443/mellon/metadata"
]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
This checks that:
Apache is running
TLS is working
Mellon loaded
SP metadata is reachable
It does not prove that the upstream IdP is functioning.
End-to-end SAML authentication requires a separate functional test.
Step 45: SAML metadata lifecycle
Treat SAML metadata as configuration that can change.
The IdP may rotate:
signing certificates
and publish the replacement through metadata.
The SP may also rotate its own:
SP signing certificate
A production process should define:
metadata refresh
certificate rollover
testing
rollback
Do not wait until the current IdP certificate expires before considering metadata lifecycle.
SP certificate versus HTTPS certificate
A Mellon deployment can have two different certificate/key pairs.
HTTPS certificate
Used by Apache TLS:
SSLCertificateFile
SSLCertificateKeyFile
Purpose:
Browser ↔ Apache TLS
SAML SP certificate
Used by Mellon:
MellonSPCertFile
MellonSPPrivateKeyFile
Purpose:
SAML signing / encryption
These are distinct roles.
Conceptually:
HTTPS
Browser ================= Apache
SAML
Apache <=================> IdP
Do not assume the same certificate must serve both purposes.
Separate keys can make lifecycle management clearer.
SAML authentication flow with Mellon
The complete SP-initiated flow looks like:
1. Browser requests protected page
↓
2. Mellon sees no authenticated session
↓
3. Apache redirects browser to IdP
↓
4. User authenticates at IdP
↓
5. IdP creates signed SAML Response
↓
6. Browser POSTs response to
/mellon/postResponse
↓
7. Mellon validates:
- response signature
- issuer
- destination
- audience
- timestamps
↓
8. Mellon creates local session
↓
9. Original request resumes
↓
10. Apache passes selected identity
information to backend
The application never processes SAML XML.
SAML versus OIDC reverse proxying
If an application already supports OpenID Connect, native OIDC is often simpler than translating SAML through Apache.
But Mellon remains valuable when:
IdP supports SAML
+
application does not
+
Apache can sit in front of application
The choice is architectural:
Native OIDC available?
|
+--- yes ---> Prefer native application integration
|
+--- no ----> Apache/Mellon can add SAML externally
A reverse-proxy SAML layer is particularly useful for:
- legacy applications;
- vendor applications;
- simple dashboards;
- internally developed apps using trusted-header authentication;
- applications where modifying authentication code is undesirable.
Apache Mellon Docker security checklist
Before production deployment, verify:
[ ] Backend application has no direct public port
[ ] Client-supplied identity headers are removed
[ ] Only Mellon-generated headers reach the application
[ ] SP private key is not committed to Git
[ ] TLS private key is not committed to Git
[ ] IdP metadata comes from a trusted source
[ ] SP metadata is registered exactly at the IdP
[ ] Entity ID is stable
[ ] ACS URLs use the externally visible HTTPS hostname
[ ] Mellon endpoint is excluded from ProxyPass
[ ] HTTPS is enforced
[ ] MellonSecureCookie is enabled
[ ] SameSite behavior is tested
[ ] SAML attributes are minimized
[ ] Group values use an unambiguous format
[ ] Multi-value attribute handling is tested
[ ] Apache configtest succeeds
[ ] Metadata endpoint works
[ ] SAML login works
[ ] Logout is tested
[ ] IdP certificate rollover is planned
Complete Docker example
Dockerfile
FROM debian:13-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
apache2 \
libapache2-mod-auth-mellon \
ca-certificates \
curl \
openssl \
&& a2enmod \
auth_mellon \
ssl \
proxy \
proxy_http \
proxy_wstunnel \
headers \
&& a2dissite 000-default \
&& rm -rf /var/lib/apt/lists/*
COPY apache.conf \
/etc/apache2/sites-available/application.conf
COPY entrypoint.sh \
/usr/local/bin/entrypoint.sh
RUN a2ensite application \
&& chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 8443
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
entrypoint.sh
#!/usr/bin/env bash
set -euo pipefail
required_files=(
/etc/apache2/mellon/sp-key.pem
/etc/apache2/mellon/sp-cert.pem
/etc/apache2/mellon/sp-metadata.xml
/etc/apache2/mellon/idp-metadata.xml
/etc/apache2/tls/server.crt
/etc/apache2/tls/server.key
)
for file in "${required_files[@]}"; do
if [[ ! -s "$file" ]]; then
echo "ERROR: Missing $file" >&2
exit 1
fi
done
chmod 600 \
/etc/apache2/mellon/sp-key.pem \
/etc/apache2/tls/server.key
apache2ctl configtest
exec apache2ctl -D FOREGROUND
compose.yml
services:
application:
image: example/application:latest
restart: unless-stopped
networks:
- application_network
saml-proxy:
build:
context: ./saml-proxy
restart: unless-stopped
depends_on:
- application
ports:
- "443:8443"
volumes:
- ./saml-proxy/mellon:/etc/apache2/mellon:ro
- ./saml-proxy/tls:/etc/apache2/tls:ro
networks:
- application_network
networks:
application_network:
The critical security property is visible immediately:
application:
no ports
saml-proxy:
port 443 exposed
Users therefore cannot bypass authentication.
Frequently asked questions
What is mod_auth_mellon?
mod_auth_mellon is an Apache HTTP Server module that implements a SAML 2.0 Service Provider.
It allows Apache to authenticate users through a SAML Identity Provider.
Can mod_auth_mellon protect an application that has no SAML support?
Yes.
Apache can authenticate the user with Mellon and then reverse proxy the request to the application.
The backend can consume an authenticated username or selected SAML attributes through trusted HTTP headers.
Can mod_auth_mellon run in Docker?
Yes.
A useful architecture is:
Apache + Mellon container
↓
private Docker network
↓
application container
Only the Apache container should normally expose a public port.
What files does Mellon need?
A normal SAML SP configuration uses:
SP metadata
SP certificate
SP private key
IdP metadata
Apache configuration
How do I generate Mellon SP metadata?
Use the metadata utility packaged with Mellon:
mellon_create_metadata \
ENTITY_ID \
ENDPOINT_URL
The exact executable name or filesystem location varies by Linux distribution.
What is MellonEndpointPath?
It defines the Apache path under which Mellon exposes its SAML endpoints.
For example:
MellonEndpointPath /mellon
creates endpoints underneath:
/mellon
including metadata and SAML response handlers.
What is MellonUser?
MellonUser selects the SAML value Apache treats as the authenticated username.
For example:
MellonUser "NAME_ID"
How do I map SAML attributes?
Use:
MellonSetEnv
For example:
MellonSetEnv "email" "mail"
which creates a Mellon environment variable for the incoming mail attribute.
How do I handle multiple group values?
Use:
MellonMergeEnvVars On ";"
to merge a multi-valued SAML attribute into one environment variable.
Choose a separator that does not occur inside the attribute values.
Can Mellon pass SAML groups to the application?
Yes.
For example:
MellonSetEnv "groups" "groups"
MellonMergeEnvVars On ";"
RequestHeader set \
X-Authenticated-Groups \
"%{MELLON_groups}e"
The backend application must understand the selected format.
Should I release LDAP memberOf directly?
Usually only if the application genuinely needs it.
A better IdP policy is often to release only application-specific groups and convert raw directory DNs into simpler values.
For example:
CN=app-users,OU=Groups,DC=example,DC=test
could become:
app-users
before reaching the SP.
Can users bypass Mellon by accessing the backend directly?
They should not be able to.
If the backend trusts authentication headers, direct backend access is an authentication bypass.
Keep the backend on a private network with no public port.
Is the Mellon certificate the same as the Apache HTTPS certificate?
Not necessarily.
The Mellon certificate is part of the SAML trust relationship.
The HTTPS certificate protects browser-to-Apache TLS.
Keeping them separate is often preferable.
Can Mellon work behind a load balancer?
Yes, but SAML endpoint URL construction must match the externally advertised HTTPS URLs.
Re-encrypting HTTPS from the load balancer to the Mellon server often avoids HTTP/HTTPS destination mismatches.
Why do I get Invalid Destination on Response?
Usually the URL in the SAML Response does not match the destination Mellon expects.
Check:
public hostname
HTTP versus HTTPS
ServerName
SP metadata
MellonEndpointPath
load balancer configuration
Why does only one group appear?
Mellon treats multi-valued attributes specially.
Check:
MellonMergeEnvVars
and the indexed environment variables Mellon produces.
Should I use SAML or OIDC?
Use the protocol supported cleanly by both your identity provider and application.
If the application already has good OIDC support, native OIDC will often be simpler.
If the application has no SSO support but can run safely behind Apache, Mellon provides a practical way to add SAML externally.
Related Mister PKI guides
- Learn about Apereo CAS Single Logout when integrating SAML applications with CAS.
- Create test TLS certificates with OpenSSL Self Signed Certificate.
- Inspect the Apache or SAML SP certificate with OpenSSL View Certificate.
- Review practical infrastructure certificate concepts in PKI for DevOps Engineers.
- Browse additional certificate and troubleshooting commands in the OpenSSL Commands Guide.
External references
- mod_auth_mellon maintained project
- Keycloak mod_auth_mellon configuration guide
- Apache mod_proxy documentation
- Apache mod_headers documentation
- Docker Compose documentation
- SAML 2.0 Technical Overview
Conclusion
Apache mod_auth_mellon is a useful tool when an application needs enterprise SAML authentication but does not implement SAML itself.
The architecture is straightforward:
User
↓
Apache
↓
mod_auth_mellon
↔
SAML Identity Provider
↓
authenticated reverse proxy
↓
Application
The main configuration pieces are:
SP metadata
SP private key
SP certificate
IdP metadata
Mellon Apache directives
For a traditional Apache deployment, Mellon can directly protect application URLs.
For Docker, a dedicated Apache/Mellon container makes an effective authentication sidecar or reverse proxy:
Internet
↓
Apache + Mellon
↓
private Docker network
↓
application
The most important security rule is that the application must not be reachable through a path that bypasses Apache.
If the backend trusts:
X-Authenticated-User
then Apache is part of the application’s authentication boundary.
Strip client-supplied identity headers, recreate them only from authenticated Mellon values, and keep the backend private.
Also pay special attention to multi-valued SAML attributes such as group membership. Mellon exposes multiple attribute values differently unless MellonMergeEnvVars is enabled, and the chosen delimiter must not conflict with the values themselves.
With those controls in place, mod_auth_mellon provides a practical bridge between a SAML Identity Provider and web applications that otherwise have no SAML support.
Leave a Reply