--- title: "GDPR on Linux Servers: Technical Measures That Matter" description: "Apply the technical side of GDPR on an Ubuntu server: map personal data, anonymize IPs in logs, set retention limits, encrypt backups and audit data access." category: "Security Compliance" language: en source: https://cubepath.com/docs/security-compliance/gdpr-compliance-on-linux-servers updated: 2026-09-25 --- # GDPR on Linux Servers: Technical Measures That Matter The EU General Data Protection Regulation (GDPR) requires anyone who processes personal data of people in the EU to protect it with "appropriate technical and organisational measures" (Article 32), keep it no longer than necessary, and be able to answer access and deletion requests. Much of that work happens on the servers that store the data. In this tutorial you will apply the most important technical measures on an Ubuntu 24.04 server running a web application: map where personal data lives, stop logging full IP addresses, enforce retention periods, encrypt data in transit and in backups, restrict and audit access, and prepare for data subject requests and breaches. > Note: This guide covers server administration, not legal advice. Decisions about legal basis, retention periods and breach notification belong to your data protection officer or legal counsel. ## Prerequisites To follow this guide you need: - A server running Ubuntu 24.04 LTS that hosts a web application with personal data, for example a CubePath VPS. - A non-root user with `sudo` privileges and SSH key access. - Nginx as the web server and MySQL or MariaDB as the database. The ideas apply to other stacks; only the commands change. - A domain name pointing to the server if you still need to set up HTTPS. - A workstation with GnuPG installed, to create the backup encryption key in Step 5. ## What GDPR expects from a server Most of the GDPR principles translate into concrete server controls: | GDPR principle or article | What it means on the server | |---------------------------|-----------------------------| | Data minimisation (Art. 5.1.c) | Do not collect or log personal data you do not need, such as full IP addresses in access logs. | | Storage limitation (Art. 5.1.e) | Delete logs, backups and records automatically when their retention period ends. | | Integrity and confidentiality (Art. 5.1.f, Art. 32) | Encrypt data in transit and at rest, restrict access, patch systems. | | Accountability (Art. 5.2, Art. 30) | Keep a record of processing activities and evidence of the controls. | | Data subject rights (Art. 15 to 20) | Be able to find, export and delete one person's data. | | Breach notification (Art. 33) | Detect breaches and report them to the supervisory authority within 72 hours. | The steps below implement each row. ## Step 1 - Mapping where personal data lives You cannot protect data you do not know about. Personal data is any information about an identifiable person: names, emails, phone numbers, IP addresses, account IDs and anything linked to them. On a typical web server it ends up in the database, web server and application logs, the system journal, uploads and backups. List the services listening on the network, since each one may receive personal data: ```bash sudo ss -tlnp ``` ```output State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1021,fd=6)) LISTEN 0 151 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=880,fd=23)) LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=702,fd=3)) ``` Find the database columns that probably hold personal data by searching the schema for typical names. Run it from the MySQL shell with `sudo mysql`: ```sql SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema NOT IN ('mysql', 'sys', 'information_schema', 'performance_schema') AND column_name REGEXP 'email|phone|name|address|birth|ip|iban|tax'; ``` Check which log files are largest and therefore likely to hold personal data such as IP addresses or emails: ```bash sudo du -sh /var/log/* | sort -rh | head ``` Record what you find in your **record of processing activities** (Article 30). For each system, write down the categories of data, the purpose, who can access it, where it is stored, whether it leaves the EU, and how long it is kept. A table in your internal wiki is enough: | System | Data | Purpose | Location | Retention | |--------|------|---------|----------|-----------| | MySQL `shop.customers` | Name, email, address | Orders and invoicing | This server | Contract + legal period | | Nginx access logs | Anonymized IP, user agent | Security and troubleshooting | `/var/log/nginx` | 14 days | | Database backups | Full database | Recovery | `/var/backups/db` + offsite | 30 days | ## Step 2 - Anonymizing IP addresses in Nginx logs IP addresses are personal data. For most troubleshooting you only need the network, not the exact host. You can make Nginx zero the last octet of IPv4 addresses and keep only the first two groups of IPv6 addresses before writing them to disk. Create a configuration file for the anonymized log format: ```bash sudo nano /etc/nginx/conf.d/anonymize-ip.conf ``` Add the following content: ```nginx map $remote_addr $remote_addr_anon { ~(?P\d+\.\d+\.\d+)\.\d+ $ip.0; ~(?P[^:]+:[^:]+): $ip::; default 0.0.0.0; } log_format anonymized '$remote_addr_anon - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent"'; access_log /var/log/nginx/access.log anonymized; ``` The default `access_log` line in the main configuration would keep writing full addresses, so disable it. Open the main file: ```bash sudo nano /etc/nginx/nginx.conf ``` Find the line `access_log /var/log/nginx/access.log;` inside the `http` block and comment it out: ```nginx # access_log /var/log/nginx/access.log; ``` If any of your `server` blocks in `/etc/nginx/sites-available/` define their own `access_log`, add `anonymized` at the end of those lines too. Test and reload Nginx: ```bash sudo nginx -t sudo systemctl reload nginx ``` ```output nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful ``` Make a request to the site and check the last log line: ```bash sudo tail -n 1 /var/log/nginx/access.log ``` ```output 203.0.113.0 - - [25/Sep/2026:10:41:12 +0000] "GET / HTTP/2.0" 200 5120 "-" "curl/8.5.0" ``` The client address now ends in `.0`. Old log files still contain full addresses until they rotate out in the next step. > Tip: If you use fail2ban or rate limiting that reads the access log, it needs full IP addresses. Keep those tools on the error log or on a separate, short-lived log with a retention of a day or two, and document why. ## Step 3 - Enforcing retention periods for logs Keeping logs forever violates storage limitation. Set a retention period for each log source and let the system delete old data automatically. Nginx logs on Ubuntu are rotated by logrotate. Check the current policy: ```bash cat /etc/logrotate.d/nginx ``` ```output /var/log/nginx/*.log { daily missingok rotate 14 compress delaycompress notifempty create 0640 www-data adm ... } ``` `daily` with `rotate 14` keeps two weeks of logs. Change the `rotate` value if your documented retention is different, and apply the same review to your application's log files. The systemd journal also stores personal data, such as SSH login IPs and application output. By default it is limited only by size. Add a time limit with a drop-in file: ```bash sudo mkdir -p /etc/systemd/journald.conf.d sudo nano /etc/systemd/journald.conf.d/retention.conf ``` ```ini [Journal] MaxRetentionSec=30day ``` Restart journald and confirm the journal's disk usage: ```bash sudo systemctl restart systemd-journald journalctl --disk-usage ``` ```output Archived and active journals take 184.0M in the file system. ``` ## Step 4 - Encrypting data in transit All traffic that carries personal data must be encrypted. For a public site, obtain a free Let's Encrypt certificate with Certbot. Replace `your_domain` with your domain: ```bash sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d your_domain -d www.your_domain ``` Certbot configures HTTPS, redirects HTTP to HTTPS and installs a timer for renewal. Check that the renewal works: ```bash sudo certbot renew --dry-run ``` Internal traffic counts too. Keep the database bound to `127.0.0.1` as shown in Step 1, or, if the application runs on another server, connect over a private network or require TLS on the database connection. Never expose port 3306 to the Internet. ## Step 5 - Encrypting backups Backups are full copies of your personal data and are often stored off the server, so they must be encrypted. Asymmetric encryption with GnuPG is a good fit: the server only holds the **public** key and can encrypt backups but never decrypt them; the private key stays offline. On your workstation, create a key pair dedicated to backups and export the public key: ```bash gpg --quick-generate-key "Backups " default default never gpg --export --armor backup@your_domain > backup-pub.asc ``` Store the private key in a password manager or offline medium. Without it, the backups cannot be restored. Copy `backup-pub.asc` to the server and import it into root's keyring, since the backup job runs as root: ```bash sudo gpg --import backup-pub.asc ``` ```output gpg: key 7C0D2B44E91A3F58: public key "Backups " imported gpg: Total number processed: 1 gpg: imported: 1 ``` Create the backup directory with restricted permissions and run a test backup. Replace `your_database` with your database name: ```bash sudo install -d -m 700 /var/backups/db sudo sh -c 'mysqldump --single-transaction your_database | gzip | gpg --batch --encrypt --recipient backup@your_domain --trust-model always -o /var/backups/db/your_database-$(date +%F).sql.gz.gpg' ``` Confirm that the file is encrypted and that the server itself cannot read it: ```bash sudo gpg --decrypt /var/backups/db/your_database-*.sql.gz.gpg > /dev/null ``` ```output gpg: encrypted with cv25519 key, ID 5B1E0C7A93D2F810, created 2026-09-25 "Backups " gpg: decryption failed: No secret key ``` The error is the expected result: only the private key on your workstation can restore the backup. Schedule the backup and the deletion of backups older than 30 days with a cron file: ```bash sudo nano /etc/cron.d/db-backup ``` ```text SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 15 2 * * * root mysqldump --single-transaction your_database | gzip | gpg --batch --encrypt --recipient backup@your_domain --trust-model always -o /var/backups/db/your_database-$(date +\%F).sql.gz.gpg 45 2 * * * root find /var/backups/db -type f -name '*.sql.gz.gpg' -mtime +30 -delete ``` The `%` signs are escaped because cron treats an unescaped `%` as a newline. Apply the same retention to any offsite copy. For data on the server's own disks, consider encryption at rest with LUKS on the volume that holds the database and uploads. It protects the data if a disk is removed or a volume snapshot leaks. ## Step 6 - Restricting and auditing access Only people who need personal data should be able to reach it, and you should be able to show who did. List accounts that can log in and members of the `sudo` group: ```bash getent passwd | awk -F: '$7 !~ /(nologin|false)$/ {print $1}' getent group sudo ``` ```output root sync sammy deploy sudo:x:27:sammy ``` Remove accounts that belong to former staff with `sudo deluser --remove-home username`, and make sure every administrator has a personal account. Shared accounts make auditing impossible. Disable SSH password logins so that only key holders can connect. Create a drop-in file: ```bash sudo nano /etc/ssh/sshd_config.d/10-hardening.conf ``` ```text PasswordAuthentication no PermitRootLogin no KbdInteractiveAuthentication no ``` Validate the configuration and restart SSH. Keep your current session open until you have confirmed a new login works: ```bash sudo sshd -t sudo systemctl restart ssh ``` Next, use the Linux audit system to record access to the files that hold personal data, such as database dumps and data exports. Install `auditd`: ```bash sudo apt install auditd ``` Create a rules file: ```bash sudo nano /etc/audit/rules.d/personal-data.rules ``` ```text -w /var/backups/db -p rwa -k personal_data -w /srv/exports -p rwa -k personal_data -w /etc/mysql -p wa -k db_config ``` Each line watches a path (`-w`) for reads, writes or attribute changes (`-p rwa`) and tags the events with a key (`-k`). Adjust the paths to your system; a watched path must exist. Load the rules and list them: ```bash sudo mkdir -p /srv/exports sudo augenrules --load sudo auditctl -l ``` ```output -w /var/backups/db -p rwa -k personal_data -w /srv/exports -p rwa -k personal_data -w /etc/mysql -p wa -k db_config ``` Read a backup file as a test, then search the audit log by key: ```bash sudo head -c 16 /var/backups/db/your_database-*.sql.gz.gpg > /dev/null sudo ausearch -k personal_data -i | tail -n 5 ``` The output shows the time, the user (`auid` is the user who originally logged in, even after `sudo`) and the command that touched the file. ## Step 7 - Handling access and erasure requests People can ask for a copy of their data (Articles 15 and 20) or for its deletion (Article 17). You usually have one month to respond, so prepare the queries in advance using the columns you mapped in Step 1. To export one person's records as JSON lines, adapt this query to your schema. Replace `your_database`, the table and columns, and the email address: ```bash sudo mysql -N your_database -e "SELECT JSON_OBJECT('id', id, 'name', name, 'email', email, 'created_at', created_at) FROM customers WHERE email = 'person@example.com'" > /srv/exports/request-1234.json ``` Deliver the file through a secure channel and delete it afterwards, since the export directory is audited and should not accumulate copies. For erasure, delete or anonymize the records in the application database, in a transaction, and document the request. Two details matter on servers: - **Backups**: you normally do not edit old backups. Instead, document that deleted data remains in encrypted backups until they expire (30 days in this example) and make sure a restore does not bring deleted records back into production. - **Secure deletion of files**: `shred` does not reliably erase data on SSDs, NVMe drives or journaling and copy-on-write file systems, because the storage writes to new blocks. Rely on encryption instead: data on an encrypted volume or in encrypted backups is unreadable once the key or the files are gone. ## Step 8 - Preparing for a data breach Under Article 33, you must notify your supervisory authority within 72 hours of becoming aware of a personal data breach, unless it is unlikely to result in a risk to people. That is only possible if you detect breaches quickly and keep enough evidence to assess them. Keep the system patched automatically: ```bash sudo apt install unattended-upgrades sudo dpkg-reconfigure -plow unattended-upgrades ``` Review successful and failed logins regularly: ```bash last -n 10 sudo journalctl -u ssh --since "24 hours ago" | grep -E "Accepted|Failed" ``` Write a short incident runbook before you need it: who decides whether an incident is a breach, who contacts the authority, and what to collect. On the server, the first actions are to preserve evidence rather than wipe it: 1. Take a snapshot or image of the affected server if your provider supports it. 2. Save the relevant logs with `sudo journalctl --since "2026-09-24" > incident-journal.txt` and `sudo ausearch -k personal_data -i > incident-audit.txt`, adjusting the date. 3. Isolate the server (firewall rules, rotated credentials) before rebuilding it. ## Conclusion You mapped the personal data on your server, stopped Nginx from storing full IP addresses, set retention limits for logs and backups, encrypted traffic and backups, restricted SSH, audited access to personal data files and prepared for data subject requests and breaches. These controls give you concrete evidence for your record of processing activities and your Article 32 documentation. As next steps, encrypt the data volume at rest with LUKS, review your processors' data processing agreements (hosting, email, analytics), and schedule a quarterly review of accounts, retention settings and audit logs.