Skip to content

fix(Runner): AJDA-3007 stop logging decrypted configuration state#805

Merged
MiroCillik merged 5 commits into
mainfrom
miro-AJDA-3007-redact-state-log
Jul 11, 2026
Merged

fix(Runner): AJDA-3007 stop logging decrypted configuration state#805
MiroCillik merged 5 commits into
mainfrom
miro-AJDA-3007-redact-state-log

Conversation

@MiroCillik

@MiroCillik MiroCillik commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary

At job start, Runner::runRow() logged the component state via json_encode($jobDefinition->getState()). By that point the state is already decrypted (upstream in job-runner's JobDefinitionFactory::decryptConfiguration()), so any #-prefixed secret it holds — OAuth access/refresh tokens, API keys — was written in plaintext to the application log channel and shipped to Datadog (hot index + S3 archive). The application log channel (getLog()) is never passed through OutputFilter, so the existing secret redaction never covered this line.

Fix

The configuration state is not useful in this log line, so it is simply removed rather than redacted (per review). The Using configuration id … notice keeps the non-sensitive fields (config id, version, row id, tmp folder):

$this->loggersService->getLog()->notice(
    'Using configuration id: ' . $jobDefinition->getConfigId() .
    ' version:' . $jobDefinition->getConfigVersion()
    . ', row id: ' . $jobDefinition->getRowId()
    . ', tmp folder: ' . $workingDirectory->getDataDir(),
);

Test

Added Tests/Runner/RunnerStateLoggingTest.php — a hermetic unit test (no Docker/network) that drives Runner::run() with a #-secret in state, halts execution right after the log line, and asserts the Using configuration id record contains neither state: nor the plaintext secret (and still logs the non-sensitive fields).

Linear: AJDA-3007

Release Notes

  • Justification

    • Decrypted component state was being logged in plaintext at job start, leaking OAuth tokens and API keys (confirmed for Shopify v2 and OneDrive; ~thousands of matching log lines across many projects/stacks/components — anything storing OAuth/tokens in state) into Datadog and its S3 archive. The state adds no diagnostic value to this line, so it is removed entirely.
  • Plans for Customer Communication

    • None required. Internal logging hygiene fix; no user-facing behavior change. Handled as a security fix; secret rotation / log scrubbing for already-exposed values is tracked separately in AJDA-3007.
  • Impact Analysis

    • Low risk. The only change is that the Using configuration id … notice line no longer includes the state. No functional/runtime behavior changes. Affects all tenants (the log line runs for every job row) but only changes what is written to logs. Not behind a feature flag (there is no behavior worth toggling — gating would mean conditionally continuing to leak secrets).
  • Deployment Plan

    • Continuous deployment via the normal docker-bundle release → job-runner bump. Not critical; no ordering constraints.
  • Rollback Plan

    • Fully reversible (two-way door): revert the commit / previous docker-bundle version. No data or schema changes.
  • Post-Release Support Plan

    • No ongoing support needed. After release, spot-check a job's logs to confirm the Using configuration id … line no longer contains state: for a component that stores OAuth tokens in state (e.g. keboola.ex-onedrive).

@linear-code

linear-code Bot commented Jul 10, 2026

Copy link
Copy Markdown

AJDA-3007

keboola-pr-reviewer-bot

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR prevents decrypted #-prefixed secrets from component state (e.g., OAuth tokens/API keys) from being logged in plaintext at job start by registering those secrets with the existing OutputFilter and redacting the application log message.

Changes:

  • Register JobDefinition state secrets with OutputFilter before emitting the Using configuration id… state: notice log line.
  • Redact the full notice message via OutputFilter::redactSecrets() (also applying existing message trimming).
  • Add a unit test that asserts state secrets are redacted ([hidden]) and not logged in plaintext.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/Docker/Runner.php Collects state secrets and redacts the “Using configuration id…” notice message before writing to the application log channel.
Tests/Runner/RunnerStateRedactionTest.php Adds a hermetic unit test ensuring state # secrets are redacted from the configuration log line.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Docker/Runner.php Outdated
Comment thread Tests/Runner/RunnerStateRedactionTest.php Outdated
@MiroCillik

Copy link
Copy Markdown
Member Author

Addressed review feedback in 2f1df22:

  • PHPUnit conventions (->method() without expects()): fixed — stubs now use expects(self::any()) (matching BaseRunnerTest) and verifyToken uses expects(self::once()).
  • Copilot: duplicate secret collection: fixed at the root by deduplicating OutputFilter::addValue() — see the inline thread.
  • Copilot: broad catch (Throwable): narrowed to RuntimeException with a message assertion.

Not changed, by design:

  • No feature-flag gate: this is a security fix that only changes what is written to the log line (secrets → [hidden]); gating it would mean conditionally continuing to leak secrets. There is no behavior worth toggling, so a flag adds risk without benefit.
  • Needs human / job-execution-core escalation: agreed, leaving for human review — the change is confined to the Using configuration id … notice line and adds no runtime behavior.

Comment thread src/Docker/Runner.php Outdated
@MiroCillik MiroCillik changed the title fix(Runner): AJDA-3007 redact decrypted state secrets from config log line fix(Runner): AJDA-3007 stop logging decrypted configuration state Jul 10, 2026
@MiroCillik

Copy link
Copy Markdown
Member Author

@odinuv vyhodil jsem teda ten state z logu uplne

@keboola-pr-reviewer-bot keboola-pr-reviewer-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: needs_human (risk 2/5) · profile ajda

Needs a human: this touches docker-bundle's job-execution core (Runner::runRow), which policy escalates regardless of size.

Concerns:

  • src/Docker/Runner.php: Modifies job-execution core (Runner::runRow) — always-needs-human per policy

Suggested reviewers: @keboola/ajda-swimlane

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comment thread src/Docker/Runner.php Outdated
Comment thread Tests/Runner/RunnerStateLoggingTest.php Outdated

@odinuv odinuv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

je to mergnutelný, ale šlo by to ještě trošku zjenodušit, pak je potřeba se ještě kouknout jestli není podobnej log někde v no dindu (IM, nebo service container)

… line

At job start runRow() logged the component state via json_encode(). By that
point the state is already decrypted, so any #-prefixed secret it held (OAuth
tokens, API keys) was written in plaintext to the application log channel and
shipped to Datadog. That channel is never passed through OutputFilter, and the
state secrets are only collected into the filter later in the StateFile
constructor - so the existing redaction missed this line entirely.

Register the state's #-keyed secrets with the OutputFilter before logging and
run the whole message through redactSecrets(), reusing the existing redaction
path. This closes both the timing and the channel gap.
…ion test

Address PR review:
- OutputFilter::addValue now deduplicates filter values. The same state is
  registered for the log line and again by StateFile, once per job row, so the
  filter list would otherwise grow unbounded across rows and make redactSecrets()
  do redundant work on every output line.
- RunnerStateRedactionTest: narrow the catch to RuntimeException and assert its
  message so the test fails loudly if a different path is reached; add explicit
  mock call-count expectations per AJDA PHPUnit conventions.
… of redacting

Per review (Odin): logging the configuration state is not necessary, so removing
it is simpler and safer than redacting. Drops the state from the 'Using
configuration id' notice entirely and reverts the OutputFilter redaction/dedup
changes that were only needed to support redaction. Test renamed to assert the
state (and its secrets) is not present in the log line.
… drop comment

Per review (Odin):
- Remove the explanatory comment above the log line (would go stale).
- Drop the standalone RunnerStateLoggingTest and instead assert in the existing
  testExecutorReadNamespacedState that the 'Using configuration id' line does not
  contain the state - avoids booting a whole extra runner test for one assert.
@MiroCillik MiroCillik force-pushed the miro-AJDA-3007-redact-state-log branch from a44f31c to 2c4917a Compare July 10, 2026 21:48
@MiroCillik MiroCillik merged commit 5b132bb into main Jul 11, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants