OpenVPN LDAP Docker Compose: Server Setup in Under an Hour

En

Setting Up OpenVPN Server with LDAP Authentication in Docker Compose

Quick answer
The working combo: an OpenVPN server based on the yyxx/openvpn image (the GavinTan/openvpn project) plus the built-in Web UI on port 8833. LDAP is turned on with a single checkbox in “System Settings” once you’ve brought up the one container from docker-compose.yml. For OpenLDAP the user attribute is uid, for Windows AD it’s sAMAccountName. Without a built-in LDAP module, you use OpenVPN plus an auth-user-pass-verify script with ldapsearch: slower to set up, but it works with any server and any UI. Below is a working compose file, the LDAP parameters, and the full path from zero to a connected client.

1. Diagnosis

You set up OpenVPN. Cut the certificates, handed out .ovpn files. A month later HR brings three new hires and asks for access “like everyone else has.” Now you either generate a certificate by hand for each one, or you remember reading something about LDAP somewhere.

Sound familiar? The task of setting up an openvpn server with ldap authentication in docker compose shows up exactly at this point: once you have more than five certificates and offboarding a person turns into a quest of “where do we even keep the revoked CRL.”

What you get at the end: a VPN server that asks for a login and password from your LDAP or Active Directory, instead of handing out access to anyone with a three-year-old .ovpn file lying around. Fire someone in AD, and they instantly can’t connect to the VPN. No extra steps needed.

True story: I once set up a VPN for a client right after a security audit found 47 active client certificates for a staff of 22. Half of them belonged to people who’d left two or three years earlier. The reason was simple: nobody kept a registry of issued certificates, and the CRL got updated once a quarter, whenever someone remembered. LDAP rules this out structurally: there’s no separate VPN user, just a reflection of whatever’s already in the company directory.

Time for the whole cycle: an hour, if LDAP is already running and you know the base DN. Three hours, if you have to figure out someone else’s directory structure by trial and error.

What you need: a server with Docker and Docker Compose, access to LDAP or AD (address, port, bind DN, base DN), an open port 1194/udp facing outside, and a port for the web interface.

In this article: the architecture of the setup, installing OpenVPN with a ready-made Web UI, turning on LDAP authentication, verifying it all works, a breakdown of common errors, an alternative without the built-in UI via auth-user-pass-verify, and maintenance: what to back up and how to update without breaking production.

One thing most articles about VPN plus LDAP skip over: the difference between authentication at the connection level and authorization at the resource level. An LDAP bind only answers “does this user exist in the directory with this password.” It doesn’t answer “does this user have the right to see the accounting subnet.” That second part is handled separately, through groups in CCD (client-config-dir) or routing on the firewall. Mixing up these two levels is a common mistake, and it’s exactly why someone from sales ends up suddenly seeing the finance department’s share.

A second thing worth understanding upfront: a client certificate and an LDAP login are two different factors, not the same mechanism under different names. The TLS certificate confirms the device is trusted (first factor), the LDAP bind confirms the person behind the device is who they claim to be (second factor). You can drop the certificate and keep only login and password, but then the VPN becomes reachable from any device if the password leaks, which is usually unacceptable for a corporate network.

A third thing: where the choice of a specific tool for this task comes from in the first place. There are dozens of ready-made Web UIs for OpenVPN on GitHub, but ones that are actually alive, support LDAP out of the box, and get updates at least every few months are rare. This article uses the one where LDAP isn’t bolted on the side as a patch from some enthusiast three years ago, but is maintained as part of the core project with an active release history. It’s not the only correct choice, but it’s a deliberate one, and the selection criteria are covered separately in the alternatives section.

OpenVPN client

OpenVPN server port 1194 udp

Certificate check

Login and password prompt

LDAP or Active Directory

Bind successful

Access granted

2. Reasons

Why LDAP and not just certificates, and what breaks if you do it the other way.

Reason to keep VPN on certificates without LDAP Why it breaks at scale
Every client gets its own .crt and .key A fired employee has to be manually revoked through the CRL. Forget to do it, and access lives forever
One shared password for everyone One leaked password means a leak for the whole company. Rotation turns into an announcement to everybody
Local user database on the VPN itself Two sources of truth: AD separately, VPN separately. They drift out of sync within a month, guaranteed
Access managed manually in a spreadsheet Nobody remembers why that user from accounting has had access since 2019. Auditing becomes impossible
One admin keeps all passwords memorized That admin goes on vacation, and nobody can grant or revoke access

LDAP or AD solves this with a single source of truth. Fire someone in AD, and within a minute (or instantly, if the session TTL is short) that person can’t log into the VPN anymore. No duplicate accounts to manage.

There’s also a practical argument for LDAP that usually surfaces only after you’ve implemented it, not before: onboarding a new employee stops requiring a network engineer. HR or IT support creates an account in AD as part of the standard hiring checklist, and the person gets VPN access the same day, with no separate “please issue me a certificate” ticket. Before LDAP, this process is almost always tied to one specific person who knows how to generate certificates, and if that person is on vacation, the new hire sits without access until they’re back.

There’s a downside too, one that marketing pages for ready-made VPN solutions tend to stay quiet about: LDAP adds a point of failure to your VPN. If the domain controller goes down, authentication to the VPN goes down with it, even if the tunnel itself is technically alive. This isn’t a reason to skip the setup, but it is a reason to keep a backup domain controller, or at least a caching LDAP proxy, nearby. Plan directory redundancy separately from VPN redundancy. These are different systems with different failure points.

Checking LDAP redundancy at the planning stage is cheaper than dealing with an incident where the whole office can’t connect on a Monday morning because the only domain controller went down for a scheduled reboot without warning. Add at least a second domain controller as a backup LDAP_URL if your UI config allows it, or keep a manual failover procedure ready for switching to a backup address.

One more reason sysadmins discover only when it’s too late: access auditing. A regulator, a security team, or plain common sense eventually asks “who logged into the VPN and when over the last quarter.” With a local user database on the VPN server itself, answering that means digging through logs by hand. With LDAP, you get a centralized authentication event log plus a login mapped to a real employee in the directory, rather than an abstract client047 someone generated two years ago and forgot who it belonged to.

3. The recipe

Preparation

Check your environment before starting. Three things, and without them there’s no point going further.


docker --version
docker compose version
Checking LDAP access
Before stacking up containers, make sure your server can actually reach LDAP at the network level. Use ldapsearch from the host. If it doesn’t respond from there, it won’t respond from inside the container either.

ldapsearch -x -H ldap://ldap.example.local:389 -D "cn=binduser,dc=example,dc=local" -w 'password' -b "dc=example,dc=local" "(uid=testuser)"

Result: you should get back the testuser record. If you get “Can’t contact LDAP server,” the problem is on the network or in the firewall, and it’s too early to move on.

System requirements

Component Version at time of publishing
Docker Engine 27.x
Docker Compose v2 (the compose plugin, not the standalone docker-compose 1.x)
OpenVPN + Web UI image yyxx/openvpn (the GavinTan/openvpn project), current release v2.5.2
Host OS Debian 12 / Ubuntu 22.04+ (any Linux with TUN support)
LDAP server OpenLDAP 2.5+ or Windows Server AD DS 2016+

Current version at publish time is v2.5.2. Before installing, check the project page for newer releases. The LDAP module gets tweaked and bugs get fixed regularly.

Port table

Port Protocol Purpose Exposed externally
1194 UDP OpenVPN tunnel Yes
8833 TCP Admin Web UI No, only through the VPN itself or behind a reverse proxy with an IP restriction
389 / 636 TCP LDAP / LDAPS to your directory No, internal network or VPN tunnel to the domain controller

Step 1. Directory and docker-compose.yml

Create a working directory and the compose file. The ./data volume stores everything: configs, certificates, the web interface’s user database.


mkdir -p /opt/openvpn && cd /opt/openvpn

services:
  openvpn:
    image: yyxx/openvpn:latest
    container_name: openvpn
    cap_add:
      - NET_ADMIN
    restart: unless-stopped
    ports:
      - "1194:1194/udp"
      - "8833:8833"
    volumes:
      - ./data:/data
      - /etc/localtime:/etc/localtime:ro

Result: the file is ready. No typos in YAML indentation, this is the first place everything silently falls apart.

Pay attention to the ./data volume. It’s not a minor detail. After the first start, it will contain the whole PKI setup (CA, server certificate, CRL), the web panel’s user database, client configs, and the LDAP configuration itself once you save it through the UI. This is, in practice, the only state that distinguishes your instance from a clean image. Lose the volume, and you lose everything. You’ll have to regenerate the PKI from scratch and send new .ovpn files to everyone at once, which means an email to the whole company and a day of support work.

If your server sits behind NAT or you’re renting a VPS from a cloud provider, open 1194/udp in the provider’s firewall or security group ahead of time, not just in iptables on the host. A forgotten rule at the cloud level is the most common reason for “the container came up but clients can’t connect” in the first half hour after deployment.

Step 2. First start


docker compose up -d
docker compose logs -f openvpn

Wait for the line about the web server starting on 8833 and the OpenVPN daemon on 1194. If the container keeps restarting in a loop, check the logs. In 90% of cases it’s a port conflict with an OpenVPN instance already running on the host.

Step 3. Logging into the Web UI and basic configuration

Open http://<server-IP>:8833. Default login and password are admin/admin. Changing the password is the first thing you do. It’s not optional.

Changing the default password
Bots scan for admin/admin across the whole internet within hours of a port being exposed. Go to “System Settings” and change the password right after your first login, before any other step.

In “Management → Clients,” generate a test .ovpn to confirm the basic certificate flow works before you touch LDAP. That way it’s easier to tell tunnel problems apart from authentication problems.

Don’t rush here. Connect with that test config from any device and check that the tunnel comes up, pings go through, and internet over VPN works, if that option is enabled. Only after that does it make sense to touch LDAP. If you start with LDAP right away and something doesn’t work, you won’t know whether it’s the certificate flow or the authentication itself that’s broken, and debugging turns into guessing instead of a sequential check.

While you’re at it, check “System Settings → OpenVPN.” That’s where the server’s basic parameters live: the client subnet, the DNS servers pushed to clients, and whether traffic is fully tunneled or only internal resources go through the VPN. By default the project runs in split-tunnel mode (access to the internal network only); full-tunnel (all client traffic through the VPN) is a separate checkbox. For corporate resource access, split-tunnel is usually enough and doesn’t load the server’s channel with someone else’s internet traffic.

Step 4. Configuring the LDAP connection

In the system settings section, turn on LDAP authentication. Once enabled, local VPN accounts in the web interface stop working for client login, so plan for that before you lock yourself out.

Parameter Value for OpenLDAP Value for Windows AD
LDAP_URL ldaps://ldap.example.local:636 ldaps://dc01.example.local:636
LDAP_USER_ATTRIBUTE uid sAMAccountName
Base DN ou=users,dc=example,dc=local ou=vpn-users,dc=example,dc=local
Bind DN cn=vpnbind,dc=example,dc=local vpnbind@example.local

LDAP_USER_ATTR_IPADDR_NAME and LDAP_USER_ATTR_CONFIG_NAME are optional fields. If you want to pin a static IP to a user through a directory attribute, use a free text field like mobile or homePhone. Most LDAP schemas leave those unused.

Use ldaps:// (TLS), not plain ldap://. User passwords travel across the network in plaintext on the bind request if TLS isn’t enabled, and that’s not a hypothetical risk, that’s literally a password in a tcpdump capture.

In practice, the difference between OpenLDAP and Active Directory in this setup comes down to three things. First, the login attribute name, already covered above. Second, the bind DN format: OpenLDAP usually wants a full DN like cn=vpnbind,dc=example,dc=local, while AD often works better and more reliably with the UPN format vpnbind@example.local instead of the classic CN path. Third, group structure: AD groups are called memberOf and sit right in the user’s attributes, while OpenLDAP more often uses a separate groupOfNames object you need to look up with a separate query. If you plan to grant access by group rather than to everyone under the base DN, figure this out ahead of time, not after you’ve accidentally given the whole company access meant for one department.

Don’t mix up base DN with bind DN. This is a classic LDAP beginner mistake. The base DN is the entry point into the directory tree, where the user search starts. The bind DN is the account the service uses to perform that search. Swap them and you’ll get either “user not found” for a user that definitely exists, or an authentication error for the service account itself.

LDAP serverOpenVPN serverClientCertificate plus login and passwordBind request over LDAPSSuccess or rejectionAccess granted or denied

Step 5. Generating a client config for an LDAP account

Once LDAP is enabled, clients log in with their directory account instead of a certificate-with-password. The certificate stays the first factor (TLS), login and password become the second. Two factors with no extra MFA setup required, which is already a decent starting point.


docker compose exec openvpn cat /data/client.ovpn

Download the file, hand it to the user, ask them to enter their AD/LDAP login when connecting. The VPN password is now the domain password. Nobody needs to remember a second password, and that’s the one reason users actually appreciate this.

Step 5.1. Encryption and tunnel parameters

By default the project generates a config with a modern cipher set: AES-256-GCM for the data channel and TLS 1.2+ for the control channel. Don’t touch these without a good reason. Outdated ciphers like BF-CBC are kept only for compatibility with ancient clients, and they bring along vulnerabilities the OpenVPN community has been writing about for a decade.

Separately, check the Diffie-Hellman key length. For new installs it’s 2048 or 3072 bits, generated automatically on first start. If you’re migrating from an old install with 1024-bit DH parameters, regenerate them. This isn’t a case where “it works, don’t touch it” applies.


docker compose exec openvpn openssl dhparam -out /data/pki/dh.pem 2048

As for full-tunnel versus split-tunnel, the choice depends on your threat model, not convenience. Full-tunnel (all client traffic through VPN) gives you control over what happens on a work laptop in public airport Wi-Fi, but it increases load on the server’s channel and needs separate DNS leak protection. Split-tunnel (only internal resources through VPN) is lighter on infrastructure but leaves the rest of the user’s traffic outside your control. For access to internal company services, split-tunnel is usually enough; full-tunnel only makes sense if you explicitly need protection for traffic on untrusted networks.

Step 6. MFA and group-based access restriction

The project supports MFA on top of LDAP. The second factor is set up by the user themselves through their personal page after logging in with their account. Turn on the MFA option for a given client in the “Clients” section, and on next login the user scans a QR code on their personal page with an authenticator app (Google Authenticator, Authy, any TOTP-compatible app). From that point on, the downloaded config includes the current TOTP parameter.

Restricting access by LDAP groups isn’t as flexible as you’d want out of the box in this project. There’s no memberOf filtering at the UI level. A working workaround: set up a separate base DN or a dedicated OU specifically for VPN users, and only add people there who are allowed access. Access control then happens through membership in that OU, not through live attribute filtering. It’s cruder than full RBAC, but it’s predictable and doesn’t break on the next image update.

If you need finer-grained routing, handle the rest at the CCD (client-config-dir) level: you can set individual push routes and subnet restrictions per client, regardless of how they authenticated.

4. Verification

Three commands that show the whole setup is alive end to end.


docker compose ps

Result: the openvpn container is Up, not Restarting.


docker compose logs --tail=50 openvpn | grep -i ldap

Result: lines about a successful bind to the LDAP server on login attempts, with no “invalid credentials” errors for a valid password.


docker compose exec openvpn cat /data/openvpn-status.log

Result: a list of active client connections showing the real LDAP username, not the certificate’s CN. If you see an actual login name rather than a generic client01, authentication is wired correctly into the chain.

A fourth check people often skip: a real test with an invalid password. Connect with a test client and deliberately enter the wrong password. The server must reject the connection with a clear authentication error, not let you in silently. Sounds obvious, but this exact check catches situations where LDAP is enabled in the UI but isn’t actually being applied, because of a typo in the verification script path, or because an old certificate-only flow keeps letting everyone through alongside the new LDAP flow.


docker compose logs --tail=20 openvpn | grep -i "denied\|invalid"

A fifth check: LDAP bind response time. If authentication takes more than two or three seconds per user, a wave of connections first thing in the morning (everyone logging in at 9:00) will swamp the server with queued bind requests. Time a single login with a stopwatch or with time around a manual ldapsearch call. Cheaper than dealing with the whole department’s complaints on Monday morning.


time ldapsearch -x -H ldaps://ldap.example.local:636 -D "cn=vpnbind,dc=example,dc=local" -w 'password' -b "ou=vpn-users,dc=example,dc=local" "(uid=testuser)"

5. Complications

Error Cause Fix Command
AUTH_FAILED right after entering the password Wrong LDAP_USER_ATTRIBUTE. For AD, people often mistakenly set uid instead of sAMAccountName Change the attribute to sAMAccountName for AD, restart the service
docker compose restart openvpn
“Can’t contact LDAP server” in the logs The container can’t reach the LDAP host over the network. Usually the firewall blocks port 636 between the Docker host and the domain controller Check port reachability from the host, open an outbound rule specifically for 636/tcp
nc -zv dc01.example.local 636
Web UI won’t open after changing the password Password was reset but the browser session is still cached with the old token Clear cookies for that domain/IP or open in a private window –
Client connects, but there’s no internet NAT/masquerading not configured on the host, or redirect-gateway is missing Add an iptables MASQUERADE rule for the tun interface, check for push redirect-gateway in the config
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
Certificate expired after a year with no warning Default client certificate lifetime is 1 year. Without EASYRSA_CERT_EXPIRE this breaks silently Set the environment variable with the desired lifetime before generating certificates
EASYRSA_CERT_EXPIRE=3650
After enabling LDAP, old local VPN accounts can’t log in This is expected behavior. LDAP fully replaces the local database for client login Add all active users to LDAP ahead of time, before flipping the switch –
Client drops exactly once an hour for no apparent reason The tls-crypt/tls-auth renegotiation key expired, or reneg-sec doesn’t match between client and server Sync the reneg-sec parameter on both sides, regenerate the client profile
grep reneg-sec /data/server.conf
Web UI responds slowly with many LDAP clients Base DN points at the whole directory instead of a specific OU, so every search scans through thousands of records Narrow the base DN down to the specific OU holding VPN users –

Nice query you’ve got there, said the LDAP server, and hung on a 30-second bind timeout, because someone forgot to index the search attribute. Check base DN indexing ahead of time, not when three hundred clients all hit a timeout at nine on a Monday morning.

A separate note on the server certificate for LDAPS. If your domain controller uses a self-signed or internal corporate CA (common with AD CS), the OpenVPN container might not trust that CA by default and will break the TLS session before it even gets to LDAP. Fix: put the internal CA’s root certificate into the container’s trusted store, or point the LDAP settings at it explicitly if the UI supports that. One openssl s_client command against the LDAPS host diagnoses this: if the TLS handshake fails with a verify error, it’s a CA trust issue, not an LDAP logic issue.


openssl s_client -connect dc01.example.local:636 -showcerts

Another practical detail: if a user keeps logging in with their old password for a while after changing it in AD, that’s not a bug in the project, it’s LDAP caching on the domain controller side or on a proxy between OpenVPN and AD. If this behavior matters to you, check the cache TTL on any intermediate LDAP proxies in the chain.

And the last thing that regularly bites people in production: the Docker host’s clock drifts more than five minutes from real time, which breaks the Kerberos part of the check (if it’s used in the path to LDAP at all) with time-related errors that look like an authorization problem but are actually a forgotten or broken NTP client on the host.


timedatectl status

6. Alternatives

If the ready-made Web UI with an LDAP module doesn’t fit, here are three working ways around it.

Option When to pick it
OpenVPN + an auth-user-pass-verify script with ldapsearch You need full control over the verification logic and are willing to write and maintain a bash script by hand
OpenVPN Access Server (official, paid beyond 2 clients) You need vendor support and a ready LDAP/SAML connector without building it yourself
WireGuard + an external identity provider (headscale, Authelia) You’re willing to drop OpenVPN for lower overhead. LDAP isn’t native here but can be bolted on through an auth proxy layer

Why this article picked yyxx/openvpn specifically: LDAP is built into the UI, no scripting dance required, MFA comes out of the box, and the project is alive, with the latest release from June 2026. For “set it up in an hour without reading Go source code,” this is the shortest path.

Money-wise, the difference is noticeable. OpenVPN Access Server is the official product from OpenVPN Inc, free for two concurrent clients, then billed per client seat on a subscription. For a team of ten to twenty people, that’s a real line item in the monthly budget, while the LDAP module there essentially does the same thing the open project does: checks a login and password against an LDAP server. WireGuard with an external identity provider is usually lighter on channel load and faster to establish a connection, but LDAP isn’t built into the protocol itself. You’ll need a separate layer like Authelia or oauth2-proxy in front of the entry point, which adds moving parts to the system and, with them, more places things can break.

If a given project’s LDAP module doesn’t fit your directory schema (nonstandard attributes, custom OUs), the auth-user-pass-verify script path remains a universal fallback. It works with any LDAP server regardless of what a specific Web UI supports.


#!/bin/bash
# /opt/app/checkpsw.sh - called by OpenVPN via auth-user-pass-verify
USERNAME="$1"
PASSWORD=$(cat "$2" | tail -n1)
ldapwhoami -x -H ldaps://ldap.example.local:636 -D "uid=${USERNAME},ou=users,dc=example,dc=local" -w "${PASSWORD}" >/dev/null 2>&1
exit $?

In server.conf, this script is hooked up with two lines, without which it never gets called. security 2 is mandatory; without it OpenVPN refuses to run external scripts by default.


script-security 2
auth-user-pass-verify /opt/app/checkpsw.sh via-file
verify-client-cert require
username-as-common-name

The upside of this path: full independence from any particular project and its release pace. The downside: you either write the whole UI yourself or accept the command line and log files instead of buttons. For a team of three admins who live in the terminal anyway, that’s not a downside at all.

One more option worth mentioning separately: the pam_ldap module paired with OpenVPN through the openvpn-auth-pam.so plugin. It gives a more standardized integration path through the system PAM stack, familiar to anyone who’s set up SSH with LDAP auth. Same tradeoff: no ready web interface out of the box, all the logic lives in /etc/pam.d configs.

7. Maintenance

Monitoring: watch the container logs for repeated AUTH_FAILED entries from the same IP. That’s either a script with a stale password or a brute-force attempt.


docker compose logs -f openvpn | grep -i "AUTH_FAILED"

Backup: the entire ./data volume. It holds the PKI, the web interface’s user database, and client configs. Without it, recovery after the host dies means regenerating the whole PKI and sending new .ovpn files to every client at once.


tar -czf openvpn-backup-$(date +%F).tar.gz /opt/openvpn/data

Frequency: once a day via cron, keep at least 14 copies off the host itself. It only takes one careless docker compose down -v overwriting the volume with zero backups on hand to wipe out the whole PKI.


0 3 * * * tar -czf /backup/openvpn-$(date +\%F).tar.gz /opt/openvpn/data

Restoring from backup: three commands, if you have a clean host with the same Docker Compose file ready. Stop the container, unpack the archive in place of the volume, bring the container back up.


docker compose down
rm -rf /opt/openvpn/data
tar -xzf openvpn-backup-2026-08-20.tar.gz -C /
docker compose up -d

Test this restore at least once before you actually need it, on a test host or a separate VM. A backup that’s never been restored has an equal chance of being fine or broken, and you’ll only find out at the moment the cost of being wrong is highest.

Separately, check the LDAP link’s failover behavior: temporarily block access to the domain controller with a firewall rule and see what happens to already-connected clients (usually nothing, the session’s already established) and to new connection attempts (they’ll predictably time out). That way you know in advance what to expect from the system during a real domain controller outage, instead of finding out mid-incident.

Autostart: restart: unless-stopped in the compose file already covers a host reboot. Verify this explicitly right after initial setup, not after the first production outage at three in the morning.


docker inspect openvpn --format='{{.HostConfig.RestartPolicy.Name}}'

Security: close port 8833 off from the outside world with a firewall rule. Access to the Web UI should only go through the VPN tunnel itself or through a reverse proxy with an IP whitelist. Set up a dedicated LDAP bind account with read-only rights. Don’t use a domain admin account for OpenVPN’s bind requests.

Measure What it gives you
UFW/firewall on 8833 The admin panel isn’t exposed externally
fail2ban on OpenVPN logs Auto-ban after N failed login attempts
Dedicated read-only bind user in LDAP A compromised OpenVPN instance can’t write to the directory
SSH hardening on the host itself Access to the host means access to the whole VPN. Protect it as your main entry point
IP restriction for the Web UI Even with a valid password, you can’t log in from outside the office network

If you do need access to the panel from an external network, say, for a remote admin, don’t expose port 8833 directly. Put nginx in front of it with basic auth and an IP restriction. A double perimeter instead of a single one: the panel won’t even be reachable through nginx without a valid client certificate.


server {
    listen 443 ssl;
    server_name vpn-admin.example.com;

    ssl_certificate     /etc/nginx/certs/vpn-admin.crt;
    ssl_certificate_key  /etc/nginx/certs/vpn-admin.key;

    allow 10.10.0.0/24;
    deny all;

    location / {
        proxy_pass http://127.0.0.1:8833;
        proxy_set_header Host $host;
    }
}

fail2ban for OpenVPN logs is set up with a filter matching failed authentication lines. There’s no ready-made jail for this specific image in the official fail2ban package, so write your own filter matching the container’s log format. Don’t mix this up: ban by source IP from the log, not by username, or one compromised password will ban a legitimate person across all their devices at once.

Updating: before updating the image, back up the volume, then pull the new version, then bring it up with the container recreated. Rollback is just putting the previous image tag back in the compose file and bringing it up again. The data volume stays untouched.


docker compose pull openvpn
docker compose up -d
docker compose logs -f openvpn

If LDAP binding breaks after an update, roll the image tag back to the last working one, and debug it in a test container, not in production.

A separate thought on logs and privacy. OpenVPN logs with LDAP authentication enabled store real employee logins, connection times, and source IP addresses. That’s personal data in the legal sense, not just abstract technical information. Decide in advance how long you keep these logs and who has access to them, especially if your company operates under rules like GDPR. Log rotation at the Docker level (max-size, max-file in the logging driver config) isn’t only about disk space, it’s also about not stockpiling personal data indefinitely without a reason.


logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "5"

8. FAQ

Why doesn’t OpenVPN work after setting up LDAP?
Most often it’s the wrong user attribute (uid instead of sAMAccountName for AD), or the LDAP port isn’t reachable from the container over the network. Check ldapsearch from the Docker host before digging deeper into the OpenVPN logs.

How do I check that LDAP authentication is working correctly?
Watch the container logs during a client login attempt, you’ll see a successful or failed bind there. The second way is the openvpn-status.log file, where a working setup shows the real username from the directory, not the certificate’s CN.

What do I do if old users can’t log in after enabling LDAP?
That’s expected behavior. Enabling LDAP disables the local database for client login. Add all active users to LDAP before flipping the switch, otherwise part of the team gets locked out during the migration.

What’s the difference between the built-in LDAP module and the auth-user-pass-verify script?
The built-in module is set up through the UI in five minutes, but it’s tied to a specific project and its config format. A script with ldapsearch is universal, works with any OpenVPN and any LDAP, but you have to write and maintain the code yourself. No magic, just bash and exit codes.

Do I have to use ldaps instead of plain ldap?
Yes. Plain ldap:// sends the user’s password in cleartext on the bind request. This isn’t a theoretical vulnerability, the password is visible in the very first tcpdump capture on the network segment between the VPN server and the domain controller.

Can I limit VPN access to a specific LDAP group?
There’s no built-in memberOf filtering in this project at the UI level. A practical workaround: create a separate OU just for VPN users and point the base DN only at it, instead of the whole directory. Finer-grained post-authentication routing is handled through client-config-dir, regardless of how someone logged in.

What do I do if the internal CA’s certificate fails verification when connecting to LDAPS?
The container doesn’t trust your internal CA’s root certificate, a common situation with Active Directory Certificate Services. Add the CA’s root certificate to the container’s trusted store, or point the LDAP settings at it explicitly if the interface has a field for that.

Do I need a separate server for LDAP, or can I use an existing domain controller?
No separate server needed. Use the existing domain controller or OpenLDAP server already serving the company. The only extra requirement is a dedicated read-only service account for the VPN, and if you need access isolation, a separate OU just for VPN users.

9. Outlook

You’ve set up OpenVPN with LDAP authentication in Docker Compose: a certificate flow on 1194/udp, a web panel on 8833 closed off from the outside world, and access through the company’s single directory. A fired employee loses VPN access the moment they lose their AD account, no more forgotten .ovpn files living their own life for three years after someone leaves.

From here it’s just operations: back up the volume daily, monitor AUTH_FAILED, update the image with a test-container check before pushing to production, and once a quarter, cross-check the list of active VPN sessions against the list of people actually still working there. Born in legacy, forged in refactoring, nothing scares it anymore, as the saying goes, but it’s better to build without legacy in the first place than to heroically clean it up at three in the morning later.

Worth doing next, beyond the scope of this article: put the LDAP bind account on a scheduled password rotation, feed VPN authentication logs into the company’s central SIEM, and after six months of running this, review the user list in the dedicated OU. Experience shows people get added quickly and removed reluctantly, and that exact habit is what led to needing this whole article in the first place.

If something didn’t come up after following this, leave a comment, we’ll sort it out.

Оставайтесь на связи

Рецепты от IT-боли. Без воды, без рекламы, без маркетинговой шелухи.

Подписаться на IT-Аптеку →

Мы ВКонтакте

IT-Аптека — советы, новости и помощь рядом.

Вступить в группу ВКонтакте →
Поделитесь:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top