turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

SEC-007

An audit log you will actually be glad you kept

Every admin action and every login, with who, what, from where. Append-only, and never editable from the panel that writes it.

The question that arrives eventually is "who changed this". Without a log, the honest answer is that you do not know, and on a site handling money that is a bad answer to give anyone.

Log the actor, the action, the target and the address, plus a short detail string. Do not log the whole request — you will end up storing passwords and API keys without meaning to.

Append only. No edit, no delete from the interface, and an admin who can clear the log can hide anything they did.

PHP
/*
CREATE TABLE audit_log (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  actor_id INT UNSIGNED NOT NULL DEFAULT 0,
  actor_kind VARCHAR(12) NOT NULL DEFAULT 'admin',
  action VARCHAR(48) NOT NULL,
  target VARCHAR(48) NOT NULL DEFAULT '',
  target_id INT UNSIGNED NOT NULL DEFAULT 0,
  detail VARCHAR(255) NOT NULL DEFAULT '',
  ip VARCHAR(45) NOT NULL DEFAULT '',
  created_at DATETIME NOT NULL,
  KEY ix_audit (created_at),
  KEY ix_audit_target (target, target_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/

function audit(PDO $pdo, string $action, string $target = '', int $targetId = 0, string $detail = ''): void
{
    try {
        $pdo->prepare(
            'INSERT INTO audit_log (actor_id, actor_kind, action, target, target_id, detail, ip, created_at)
             VALUES (?, ?, ?, ?, ?, ?, ?, NOW())'
        )->execute([
            (int) ($_SESSION['admin_id'] ?? 0),
            isset($_SESSION['admin_id']) ? 'admin' : 'system',
            $action,
            $target,
            $targetId,
            mb_substr($detail, 0, 255),
            client_ip(),
        ]);
    } catch (Throwable $e) {
        // Logging must never take down the action it was recording.
    }
}

// audit($pdo, 'balance_adjust', 'user', 412, '+50000 units: refund for failed payout');
// audit($pdo, 'login_ok');
// audit($pdo, 'setting_change', 'setting', 0, 'credit_units 100 -> 120');

Using it

Log the before and after value on anything numeric. "Changed the rate" is nearly useless; "100 to 120" answers the question on its own.

Give the log its own admin page with a filter by action and by target, and no delete button anywhere on it.

What bites people

Never log passwords, API keys, tokens or full request bodies. A log that becomes a credential store is worse than no log.

Swallow logging failures. A full disk or a missing table should not stop a payout from being recorded.

Also in Auth and Security