Hardening a Two-Server Teampass Setup on Debian with Fail2ban

Self-hosted password managers like Teampass are a great fit for teams that need full control over their secrets. But once you deploy the application, the job is not done: you still need to harden the servers, protect the  database, and block brute-force attacks.

In this guide we will walk through a practical hardening process for a two-server Teampass deployment on Debian, including:

  • System updates and SSH hardening
  • Firewall rules with UFW for app and DB servers
  • MySQL (or MariaDB) hardening
  • Nginx and PHP security tuning
  • Locking down Teampass files and folders
  • Fail2ban jails for SSH, MySQL and Nginx/Teampass

All examples are written for Debian 13 (or similar), but the same concepts apply to most modern Linux distributions.


Reference architecture

For the rest of the article we assume the following simple layout:

  • DB server: 10.0.0.10 – MySQL 8 / MariaDB
  • APP server: 10.0.0.11 – Nginx + PHP-FPM + Teampass
  • Both servers run Debian 13
  • Application files live in /var/www/teampass (example)
  • Secure path for Teampass attachments in /var/teampass-secure

You can adjust IP addresses, domain names and paths to match your own environment.
The security principles remain the same.


Step 1: Keep the system updated

Before touching configuration files, always make sure the base system is up to date. Security patches for the kernel and system libraries are just as important as the application itself.

sudo apt update && sudo apt upgrade -y

If you want to automate security updates, install and enable unattended-upgrades:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades

Step 2: SSH hardening on both servers

SSH is usually the main remote entry point to your servers.
Hardening it significantly reduces the attack surface.

  1. Disable direct root login.
  2. Use SSH keys instead of passwords.
  3. Limit what SSH can do (no X11, no useless forwarding, etc.).

Edit the SSH configuration file on both servers:

sudo vim /etc/ssh/sshd_config

Recommended settings:

Port 22
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
X11Forwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2

Before you disable password authentication,
make sure you can log in using SSH keys from at least one admin machine.

sudo systemctl restart ssh

Step 3: Firewall with UFW

A firewall should enforce the idea of “only what is needed is allowed”. We will use UFW (Uncomplicated Firewall), which provides a simple syntax.

3.1 DB server firewall (10.0.0.10)

Goal:

  • Allow SSH only from your admin network
  • Allow MySQL only from the APP server (10.0.0.11)
  • Block everything else
sudo apt install -y ufw

# default policy
sudo ufw default deny incoming
sudo ufw default allow outgoing

# SSH from admin subnet (adjust to your real network)
sudo ufw allow from 10.0.0.0/24 to any port 22 proto tcp

# MySQL only from APP server
sudo ufw allow from 10.0.0.11 to any port 3306 proto tcp

sudo ufw enable
sudo ufw status verbose

3.2 APP server firewall (10.0.0.11)

Goal:

  • Expose only HTTP/HTTPS to the outside world
  • Allow SSH only from admin network
sudo apt install -y ufw

sudo ufw default deny incoming
sudo ufw default allow outgoing

# Web traffic
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# SSH from admin subnet
sudo ufw allow from 10.0.0.0/24 to any port 22 proto tcp

sudo ufw enable
sudo ufw status verbose

With this in place, your database is not exposed to the internet at all, and the app server exposes only the ports it really needs.


Step 4: Hardening MySQL on the DB server

Teampass stores all secret metadata in the database. Protecting the database server is critical.

4.1 Run mysql_secure_installation

If you have not done it already,
run the standard MySQL hardening script to set a strong root password, remove anonymous users and test databases:

sudo mysql_secure_installation

4.2 Bind MySQL only to the internal address

By default, MySQL may listen on 0.0.0.0 (all interfaces). We want it to listen only on the internal address of the DB server.

sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf

Update or add the following line in the [mysqld] section:

bind-address = 10.0.0.10

Restart MySQL:

sudo systemctl restart mysql

4.3 Disable risky features

In the same mysqld.cnf, add:

local_infile = 0
symbolic-links = 0

Disabling local_infile reduces the risk of certain file-reading attacks, and disabling symbolic-links can help avoid some filesystem abuses.

Restart again after changes:

sudo systemctl restart mysql

4.4 Restrict Teampass database user

Teampass should have its own dedicated database user with access only to its own database.

sudo mysql -u root -p
GRANT ALL PRIVILEGES ON teampass.* TO 'teampass'@'10.0.0.11' IDENTIFIED BY 'VeryStrongPasswordHere';
FLUSH PRIVILEGES;

Replace the password with a strong unique value. Do not reuse this user for any other application.


Step 5: Hardening Nginx and PHP on the APP server

5.1 Hide Nginx version

Edit the main Nginx configuration:

sudo vim /etc/nginx/nginx.conf

Inside the http { ... } block add:

server_tokens off;

This prevents Nginx from revealing its version in error pages and headers, making fingerprinting slightly harder for an attacker.

5.2 Add security headers to the Teampass virtual host

Edit your Teampass server block, for example:

sudo vim /etc/nginx/sites-available/teampass.conf

Add the following lines inside the server { ... } block:

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;

If your site is served over HTTPS, you can also enable HSTS:

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Test and reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

5.3 PHP-FPM security tuning

Edit the PHP configuration file (version may differ):

sudo vim /etc/php/8.4/fpm/php.ini

Recommended changes:

expose_php = Off
display_errors = Off
log_errors = On

session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1

disable_functions = exec,passthru,shell_exec,system,proc_open,popen,fsockopen
  • expose_php = Off hides the PHP version from headers.
  • display_errors = Off prevents leaking paths and details to users.
  • session.cookie_secure = 1 ensures session cookies are sent only over HTTPS (keep this on for production).
  • disable_functions removes dangerous native functions that Teampass does not need.

Reload PHP-FPM:

sudo systemctl reload php8.4-fpm

Step 6: Lock down Teampass files and secure path

Teampass uses a separate secure directory for sensitive data (attachments, keys). We want the web server to access it, but no one else.

# Application files
sudo chown -R www-data:www-data /var/www/teampass
sudo find /var/www/teampass -type d -exec chmod 750 {} \;
sudo find /var/www/teampass -type f -exec chmod 640 {} \;

# Secure path
sudo chown -R www-data:www-data /var/teampass-secure
sudo chmod -R 750 /var/teampass-secure

After the installation is done, remove or lock down the installer directory and config file:

cd /var/www/teampass

# Remove install directory (if still present)
sudo rm -rf install

# Make the main settings file read-only for the web user
sudo chmod 640 includes/config/settings.php 2>/dev/null || true

This way, even if someone manages to upload a malicious PHP script somewhere, file permissions will limit the impact.


Step 7: Install Fail2ban on both servers

Even with strong passwords and SSH keys, you will still see automated login attempts and scans in your logs. Fail2ban watches logs for suspicious patterns and dynamically bans offending IP addresses.

Install Fail2ban on both servers:

sudo apt install -y fail2ban

By default, Fail2ban ships with a global jail.conf. You should not edit this file directly. Instead, create /etc/fail2ban/jail.local and place your custom configuration there.


Step 8: Fail2ban on the DB server (SSH + MySQL)

Create or edit /etc/fail2ban/jail.local:

sudo vim /etc/fail2ban/jail.local

Example configuration:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd
ignoreip = 127.0.0.1/8 10.0.0.0/24

[sshd]
enabled  = true
port     = 22
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 5

[mysqld-auth]
enabled  = true
port     = 3306
filter   = mysqld-auth
logpath  = /var/log/mysql/error.log
maxretry = 5
  • [DEFAULT] defines global settings for all jails: ban time, retry window, etc.
  • ignoreip contains IP ranges that should never be banned (your internal network, VPN, etc.).
  • [sshd] watches SSH logins in /var/log/auth.log.
  • [mysqld-auth] watches authentication failures in the MySQL error log.

Enable and check Fail2ban:

sudo systemctl enable fail2ban
sudo systemctl restart fail2ban

sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client status mysqld-auth

Step 9: Fail2ban on the APP server (SSH + Nginx/Teampass)

Edit /etc/fail2ban/jail.local on the APP server:

sudo vim /etc/fail2ban/jail.local

Start with basic SSH and Nginx protection:

[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
backend  = systemd
ignoreip = 127.0.0.1/8 10.0.0.0/24

[sshd]
enabled  = true
port     = 22
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 5

[nginx-http-auth]
enabled  = true
port     = http,https
filter   = nginx-http-auth
logpath  = /var/log/nginx/*error.log
maxretry = 5

The nginx-http-auth jail protects endpoints that use HTTP auth (if you use it), but we can go further and track suspicious login attempts to Teampass itself.

9.1 Optional: Custom Fail2ban filter for Teampass logins

If your Teampass logs go into a dedicated access log (for example /var/log/nginx/teampass_access.log),
you can create a custom filter to ban IPs that make too many login attempts in a short period of time.

First, check a few log lines during failed login attempts and see how they look. Then create a filter file:

sudo vim /etc/fail2ban/filter.d/teampass-login.conf

Example (simple pattern based on POSTs to index.php):

[Definition]
failregex = <HOST> - .*POST /index\.php HTTP/.*" 200
ignoreregex =

This is just a starting point.
You should adjust the regex to match the real log format of invalid login attempts for your Teampass instance.

Next, add the jail to jail.local:

[teampass-login]
enabled  = true
port     = http,https
filter   = teampass-login
logpath  = /var/log/nginx/teampass_access.log
maxretry = 10
findtime = 10m
bantime  = 1h

Restart Fail2ban and check the status:

sudo systemctl restart fail2ban

sudo fail2ban-client status
sudo fail2ban-client status teampass-login

Step 10: HTTPS is a must

Because Teampass handles highly sensitive data, HTTPS is mandatory. If you have a public domain pointing to your APP server, you can easily obtain a free TLS certificate with Let’s Encrypt and Certbot.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d teampass.example.com

Certbot will request the certificate, update your Nginx configuration, and optionally redirect all HTTP traffic to HTTPS. This works perfectly together with the HSTS header and the session.cookie_secure setting.


Monitoring and next steps

After implementing all these steps, your Teampass servers are significantly better protected than a default installation. But security is not a one-time task — it is a process.

Make a habit of checking:

  • Fail2ban status and banned IPs:
    sudo fail2ban-client status
  • System updates at least weekly
  • Nginx and PHP logs for unexpected errors
  • Database logs for suspicious access

You can further improve your setup by integrating central logging (rsyslog/ELK), backups with encryption, and monitoring using Prometheus or similar tools.


Conclusion

A two-server Teampass deployment on Debian is a solid foundation for a self-hosted password manager in a small or medium-sized organization. By combining SSH hardening, firewall rules, MySQL hardening, Nginx/PHP security tuning and Fail2ban on both servers, you significantly raise the bar for any attacker.

Use this guide as a baseline, adapt the IP ranges and domain names to your environment, and keep iterating on your security posture as your infrastructure grows.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.