MySQL, MariaDB and PostgreSQL Logs: Linux Database Forensics
Database server evidence on Linux: MySQL/MariaDB error, general and binary logs, PostgreSQL server logs, client history files and the plugins attackers abuse.
- Location
- /var/log/mysql/, /var/log/mariadb/, /var/lib/mysql/ (binlogs), /var/log/postgresql/ (Debian), /var/lib/pgsql/data/log/ (RHEL)
- Distributions
- Proves
- Database logins and failures, statements executed (when logged), data changes in binary logs, and abuse such as file writes or command execution through the database
- Timestamps
- MySQL 8 error log ISO 8601 UTC by default; MariaDB and PostgreSQL local time unless configured; binlog events in Unix seconds
- Access
- root or the mysql/postgres service account; client history files owned by each user
- Retention
- Error logs follow logrotate or overwrite schedules; MySQL 8 binlogs expire after 30 days by default; general/query logging is off by default
- Collection
- UAC, Velociraptor, cp -a, tar
Tools
Compare all tools- mysqlbinlogCLI · open source
- pgBadgerCLI · open source
- grep / zgrepCLI · built into Linux
- journalctlCLI · built into Linux
What it is
Databases on Linux web and application servers are both targets (data theft, ransom notes left in tables) and tools (SQL injection that writes a web shell with SELECT ... INTO OUTFILE, PostgreSQL COPY ... FROM PROGRAM that runs shell commands as postgres, MySQL user-defined functions loaded from a shared library). The server logs, binary logs and client history files are how you reconstruct that.
What exists depends heavily on configuration. By default, neither MySQL nor PostgreSQL logs every statement, so check the configuration first to know what you can expect to find.
Where it lives
| Item | Debian / Ubuntu | RHEL family | Notes |
|---|---|---|---|
| MySQL error log | /var/log/mysql/error.log | /var/log/mysql/mysqld.log (AppStream) or /var/log/mysqld.log (Oracle RPMs) | log_error in my.cnf |
| MariaDB error log | journal by default (log_error commented out), or /var/log/mysql/error.log | /var/log/mariadb/mariadb.log | |
| General query log | general_log_file, often <datadir>/<host>.log | same | Off by default; every connection and statement when on |
| Slow query log | slow_query_log_file | same | Off by default |
| Binary logs | /var/lib/mysql/binlog.000N (MySQL 8) or mysql-bin.000N | same | On by default in MySQL 8.0+, off in MariaDB |
| Audit plugins | MariaDB server_audit.log, Percona/MySQL Enterprise audit.log in datadir | same | Only if installed |
| Plugin directory | /usr/lib/mysql/plugin/ | /usr/lib64/mysql/plugin/ or /usr/lib64/mariadb/plugin/ | UDF shared libraries |
| PostgreSQL log | /var/log/postgresql/postgresql-<ver>-<cluster>.log | /var/lib/pgsql/data/log/postgresql-<Day>.log or /var/lib/pgsql/<ver>/data/log/ | logging_collector, log_directory in postgresql.conf |
| Client history | ~/.mysql_history, ~/.psql_history | same | Including /root, /var/lib/mysql, /var/lib/postgresql or /var/lib/pgsql |
| Configuration | /etc/mysql/, /etc/postgresql/<ver>/<cluster>/ | /etc/my.cnf, /etc/my.cnf.d/, postgresql.conf in the data directory |
What it proves
- Connection attempts and authentication failures (
Access denied for user 'root'@'203.0.113.50'in MySQL/MariaDB;password authentication failed for userin PostgreSQL), when the verbosity allows it. - Errors produced by SQL injection probes, which PostgreSQL logs with the failing statement by default (
log_min_error_statement = error). - Data changes with exact statements or row images, from binary logs, even when nothing else logged them.
- Operating system actions through the database: files written by
INTO OUTFILE/DUMPFILE, programs started byCOPY FROM PROGRAM, libraries loaded withCREATE FUNCTION ... SONAME. - It does not prove which rows were read (
SELECT) unless the general log,log_statement = 'all'or an audit plugin was enabled.
Key fields
# MySQL 8 error log (log_error_verbosity 3)
2026-09-20T03:14:07.512031Z 812 [Note] [MY-010926] [Server] Access denied for user 'root'@'203.0.113.50' (using password: YES)
# PostgreSQL (log_line_prefix '%m [%p] %q%u@%d ')
2026-09-20 05:14:09.118 CEST [4302] app@shop ERROR: syntax error at or near "UNION" at character 57
2026-09-20 05:14:09.118 CEST [4302] app@shop STATEMENT: SELECT * FROM items WHERE id=1' UNION SELECT ...
| Item | Meaning |
|---|---|
MySQL thread ID (812) | Connection; follow it through the general log |
[MY-xxxxxx] | Error code, for example MY-010926 for access denied and MY-010914 for aborted connections |
PostgreSQL %u@%d, [%p] | User, database, backend PID (only if in log_line_prefix) |
Binlog # at N, #YYMMDD hh:mm:ss server id | Event offset and time; SET TIMESTAMP= is epoch seconds |
Timestamps
MySQL 8 writes error log times in UTC with a Z suffix by default (log_timestamps = UTC). MariaDB, older MySQL and PostgreSQL use the server's local time unless configured otherwise; PostgreSQL prints the zone abbreviation when %m or %t is used. Binary log headers show local time, but SET TIMESTAMP=<epoch> in the decoded output is authoritative.
Retention
MySQL 8 purges binary logs after binlog_expire_logs_seconds (2,592,000 seconds, 30 days, by default). Error logs rotate through distribution logrotate snippets. On RHEL, PostgreSQL's default log_filename = 'postgresql-%a.log' with log_truncate_on_rotation = on overwrites each weekday's file every week, so only seven days survive. Debian's PostgreSQL logs rotate weekly through logrotate.
Collection
tar -C /mnt/evidence -cpf /cases/2026-017/db.tar etc/mysql etc/my.cnf etc/my.cnf.d etc/postgresql \
var/log/mysql* var/log/mariadb var/log/postgresql var/lib/pgsql/data/log var/lib/pgsql/*/data/log \
var/lib/mysql/*bin* var/lib/mysql/*.log usr/lib/mysql/plugin usr/lib64/mysql/plugin usr/lib64/mariadb/plugin \
root/.mysql_history root/.psql_history home/*/.mysql_history home/*/.psql_history \
var/lib/mysql/.mysql_history var/lib/postgresql/.psql_history var/lib/pgsql/.psql_history 2>/dev/null
UAC's var_log artifact covers logs under /var/log; data-directory logs and binlogs need a custom collector. Collect from a copy or snapshot; do not start the database on the evidence.
Parsing
# Binary logs, row events decoded, one time window
mysqlbinlog --base64-output=DECODE-ROWS -v --start-datetime='2026-09-20 03:00:00' \
--stop-datetime='2026-09-20 05:00:00' binlog.000042 > binlog_decoded.sql
# MySQL/MariaDB: failures, dangerous functions and file writes
zgrep -hE 'Access denied|INTO (OUT|DUMP)FILE|LOAD_FILE|CREATE FUNCTION|SONAME' mysql/*.log*
# PostgreSQL: auth failures and command execution
grep -hE 'authentication failed|FROM PROGRAM|lo_export|CREATE EXTENSION|ALTER SYSTEM' postgresql*.log
pgbadger -f stderr postgresql-*.log -o pg_report.html
Investigator tips
- New files in the plugin directory (
lib_mysqludf_sys.soand look-alikes) andCREATE FUNCTION sys_execin binlogs mean OS command execution as themysqluser. Correlate with SELinux or AppArmor denials formysqld. COPY ... FROM PROGRAMruns commands aspostgres; look at that account's home directory, shell history and cron entries, and for processes whose parent is apostgresbackend.- Files written with
INTO OUTFILEare owned bymysqland often land in the web root; pair their birth time with web server logs. ~/.mysql_historyescapes spaces as\040on builds linked with libedit, and the MySQL client skips statements matching its default ignore patterns (*IDENTIFIED*:*PASSWORD*), so passwords are usually absent.- Enabling the general log or
log_statement = 'all'after an incident starts collecting; note the time you changed it so your own actions are not mistaken for the attacker's.