Skip to content

fix(log): keep secrets out of ee.log and make the log readable by root only - #1944

Merged
mrrobot47 merged 9 commits into
EasyEngine:developfrom
mrrobot47:fix/mask-secrets-in-logs
Oct 12, 2026
Merged

mrrobot47 merged 9 commits into
EasyEngine:developfrom
mrrobot47:fix/mask-secrets-in-logs

Conversation

@mrrobot47

Copy link
Copy Markdown
Member

Values passed in $obfuscate are now also masked in the logged output of the command, and EE::obfuscate() masks values in everything logged from then on: command lines, their output, ee.log messages and context, and debug output. Secret flags, secret config values and the secret columns of loaded models (site and auth passwords) are registered automatically, and the logged command line shows secret flags as ******. As a fallback, secret flags, JSON members, mysql -p and openssl pass: values are masked by pattern. What commands print for the user is unchanged.

A new logs directory is created 0700 and ee.log 0600, with the modes set explicitly rather than relying on the umask. A migration restricts existing log files (including rotated ones) and masks secrets that earlier versions wrote to them; each file is rewritten to a checked copy before it replaces the original, and a file that can't be rewritten keeps its content (0600) with a warning, without failing the upgrade. CI reads the logs with sudo.

Tested with PHPUnit (PHP 7.4 and 8.5), a log harness, CI and live on a server with existing logs.

A new logs directory is created with mode 0700 and a new ee.log with 0600, under a 0077 umask, so the file is never readable by other users, not even briefly. Existing modes are left as they are; a migration tightens them once.
Values passed in $obfuscate are now also masked in the logged STDOUT and STDERR of the command, not only in its command line. EE::obfuscate() registers values to mask in everything logged from then on: command lines and output of EE::exec() and EE::launch() (and so docker_compose_exec()), every message and context value written to ee.log, and debug output. What commands print for the user is unchanged.

At startup the values of secret flags (--admin-pass, --dbpass, --pass, --password, --dbrootpass, --dash-auth, --api, and other names ending in pass, password, secret, token or api-key), the value of 'config set <secret-key> <value>' and the values of secret keys in config.yml are registered, and the logged argv line shows them as ******. Logged context arrays have the values of secret keys masked.

As a fallback for values EE doesn't know, such as JSON from another host, '--<secret>=value' flags and '"<secret>": "value"' JSON members are masked by pattern.
A migration sets the logs directory to 0700 and every file in it, including rotated and compressed logs, to 0600. It then rewrites the logs with the secrets masked: the passwords of existing sites and auth users and the secret config values, plus the shapes in which older versions logged secrets (secret flags, JSON members, config set values, the values printed by config get and config list, failed CREATE USER statements, wp config set output, mysql -p and openssl pass: values). Plain logs are rewritten in place, so their inode, owner and mode stay; compressed logs are replaced and keep their modification time. A file that can't be rewritten is left as it is, with a warning, and the migration never fails the upgrade.
The logs directory is now created readable by root only, so the log output step reads it with sudo.
…tput

Site, auth and other model rows register their secret columns (and secret options such as the self-signed CA password) with EE::obfuscate() when they are loaded, created or updated, so every command masks them without each caller passing them. The pattern fallback now also covers mysql -p<password>, openssl pass:<password> and numeric JSON values, keeps single-quoted values that start with $ masked, and uses possessive quantifiers so a very long value can't make the regex fail and skip masking. The token part of a secret flag value such as --dash-auth=<id>:<token> is registered too. Masking uses one cached strtr() map instead of sorting the values for every logged line, and objects in log context are masked after conversion to text.
The log migration now writes each masked copy next to the log, checks that every write succeeded and that the copy reads back with the same number of lines, and only then renames it over the log, so a full disk can't leave a truncated log. The running process's handler is closed after ee.log is replaced, so its next lines go to the new file. It also masks the EasyDash token debug lines of older versions, and reads the config through a shared helper.
… umask

A new ee.log and the masked copy that replaces a scrubbed log got 0600 only through umask(0077). A default ACL on the logs path (as on GitHub runners' /opt) overrides the umask, so they came out group/world readable after the scrub (0666 there).

create_log_file() now chmods a log it creates to 0600, and scrub_file() chmods the masked copy to 0600 before renaming it over the log, failing the scrub like any other write error when it can't. The logs dir itself is fine: mkdir()'s 0700 mode still limits a default ACL.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Valid argument and escaping cases can still expose secrets, while migration read failures may replace logs with partial copies.

5 open findings
What changed in this PR

Secures EasyEngine logging by masking secrets and restricting log-file access.

Changes:

  • Adds centralized secret detection and masking.
  • Registers secrets from arguments, configuration, and models.
  • Migrates existing logs to private permissions and scrubbed contents.
File Description
php/​utils.php Adds secret detection and masking helpers.
php/​EE/​Runner.php Creates private logs and registers startup secrets.
php/​EE/​Model/​Option.php Obfuscates secret option values.
php/​EE/​Model/​Base.php Registers model secrets during data operations.
php/​class-ee.php Masks command, output, context, and debug logging.
migrations/​db/​20261001100000_easyengine_secure_log_files.php Secures and scrubs existing logs.
.github/​workflows/​test_and_build.yml Reads private logs using sudo.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread php/class-ee.php
Comment thread php/utils.php Outdated
Comment thread php/utils.php Outdated
Comment thread migrations/db/20261001100000_easyengine_secure_log_files.php
Comment thread php/utils.php
…g config values, and keep logs with read errors

A command's own obfuscate values are also masked in their escapeshellarg() and JSON forms. `config set <secret> <value>` is masked after an @alias and when the value starts with a single dash. The old-log migration keeps a file whose read or decompression fails instead of replacing it with a partial copy.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Some configuration secrets can remain unregistered, short values can leak, and concurrent writers can lose log entries during migration.

0 open findings

5 resolved since last review
Previously missed (5)

In code that hasn't changed since last review

Medium severity Coordinate log migration with concurrent writers

migrations/​db/​20261001100000_easyengine_secure_log_files.php:200

Closing this process's handler does not protect concurrent EE invocations. Another process that already has ee.log open will continue appending to the old, now-unlinked inode after rename(), and writes made after this scrubber reaches EOF can disappear from the visible log. Coordinate the migration with all log writers or use an active-log rewrite/rotation strategy that preserves the inode (the existing logrotate path uses copytruncate).

Medium severity Fail closed when secure log file creation or restriction fails

php/​EE/​Runner.php:645

The privacy guarantee is silently bypassed when touch() or chmod() fails: initialization continues and StreamHandler can write to a file whose inherited ACL/mode is still readable by other users. Since this is the creation path for a new log, fail closed (and restore the umask) unless the file can be created and restricted successfully.

Medium severity Register secrets from merged effective configuration

php/​EE/​Runner.php:796

This reads only the global config file, although init_config() also merges inherited and project-level ee.yml/ee.local.yml values into $this->config and $this->extra_config (Runner.php:668-696). A project-level cloudflare-api-key, for example, is therefore never registered and can remain visible when later logged. Register secrets from the already-merged effective configuration instead.

Medium severity Handle short bare secret values with context-aware masking

php/​class-ee.php:577

Discarding every known secret shorter than six characters still permits concrete leaks when the value is logged without its key. For example, a short secret config value printed by config get reaches an INFO message as the bare value, which none of the fallback key/flag patterns can recognize; notably, the migration has dedicated state to scrub this exact historical shape. Preserve the anti-overmasking behavior only with context-aware handling of bare secret outputs rather than dropping these secrets globally.

Medium severity Collect secret values from nested command configuration

php/​utils.php:1522

Command-specific config is nested (for example, $this->extra_config[$name] is passed into commands in Runner.php:376-384), but this collector examines only top-level scalar values. A config entry such as site create: { admin-pass: ... } is not registered, so the automatic masking promised for secret config values does not apply. Recurse into nested config arrays while collecting secret-key values.

🧠 Review effort: Balanced

@mrrobot47
mrrobot47 merged commit f87381b into EasyEngine:develop Oct 12, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants