Skip to content

fix(jit): mask credentials in GitHub Actions logs via ::add-mask::#262

Open
amaechiabuah wants to merge 1 commit into
mainfrom
fix/jit-mask-secrets-in-github-actions
Open

fix(jit): mask credentials in GitHub Actions logs via ::add-mask::#262
amaechiabuah wants to merge 1 commit into
mainfrom
fix/jit-mask-secrets-in-github-actions

Conversation

@amaechiabuah

Copy link
Copy Markdown
Collaborator

Describe Changes

When GITHUB_ACTIONS=true, the jit commands now register their secret-bearing fields with the runner's masking system by emitting ::add-mask::<value> workflow commands to stderr. This stops AWS credentials (and the GCP / Argo / k8s / JWT tokens) from appearing in plain text in subsequent step logs.

  • aws masks AccessKeyId, SecretAccessKey, SessionToken, ConsoleUrl
  • gcp masks Token
  • argo_wf masks Token
  • k8s masks status.token
  • token masks the JWT

stderr is used so the masks survive >> $GITHUB_OUTPUT (which only redirects stdout); the runner reads workflow commands from both streams.

End-to-end verification on a live runner: https://github.com/duplocloud/actions/actions/runs/25868854856 — the aws-actions/configure-aws-credentials@v6 with: block now shows aws-access-key-id: ***, aws-secret-access-key: ***, aws-session-token: ***, while aws-region: us-west-2 is correctly left un-masked, and aws sts get-caller-identity succeeds (creds are intact, only their log representation is redacted).

Link to Issues

  • ClickUp: DUPLO-42934
  • DuploCloud Support ticket #41911 (Slack thread, Tiffany Tang)

PR Review Checklist

  • Thoroughly reviewed on local machine.
  • Have you added any tests
  • Make sure to note changes in Changelog

Test plan

  • 5 new unit tests in src/tests/test_jit.py covering: emission to stderr, empty/None skipping, no-op outside GITHUB_ACTIONS, no-op when GITHUB_ACTIONS=false, case-insensitive TRUE trigger
  • Full unit suite: 285 pass (1 pre-existing failure in test_aws_plugin.py::test_client_entry_point_registered, reproduces on clean origin/main with this branch stashed — not a regression)
  • ruff check src/duplo_resource/jit.py src/tests/test_jit.py clean
  • End-to-end live runner verification against test24.duplocloud.net / duplo-tools tenant: secrets masked, creds still functional

@zafarabbas

Copy link
Copy Markdown
Contributor

@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Mask JIT credentials in GitHub Actions logs via ::add-mask::

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Mask AWS, GCP, Argo, k8s, and JWT credentials in GitHub Actions logs
• Emit ::add-mask:: workflow commands to stderr for secret redaction
• Skip empty/None values and only activate when GITHUB_ACTIONS=true
• Add comprehensive unit tests covering all masking scenarios
Diagram
flowchart LR
  A["JIT Commands<br/>aws, gcp, argo_wf, k8s, token"] -->|"Extract secrets"| B["_mask_in_ci()"]
  B -->|"Check GITHUB_ACTIONS=true"| C{"CI Environment?"}
  C -->|"Yes"| D["Emit ::add-mask::<br/>to stderr"]
  C -->|"No"| E["No-op"]
  D -->|"Runner reads"| F["Redact secrets<br/>in logs"]
  E -->|"Return"| G["Credentials unchanged"]
Loading

Grey Divider

File Changes

1. src/duplo_resource/jit.py 🐞 Bug fix +34/-0

Add credential masking for GitHub Actions logs

• Added _mask_in_ci() helper function to emit ::add-mask:: workflow commands to stderr
• Integrated masking calls in token(), gcp(), argo_wf(), aws(), and k8s() methods
• Masks AWS credentials (AccessKeyId, SecretAccessKey, SessionToken, ConsoleUrl), GCP Token, Argo
 Token, k8s token, and JWT token
• Only activates when GITHUB_ACTIONS environment variable is set to true (case-insensitive)

src/duplo_resource/jit.py


2. src/tests/test_jit.py 🧪 Tests +50/-0

Add unit tests for credential masking

• Added 5 new unit tests for _mask_in_ci() function
• Tests verify workflow commands emitted to stderr, empty/None value skipping, no-op outside GitHub
 Actions, no-op when GITHUB_ACTIONS=false, and case-insensitive TRUE trigger
• All tests marked with @pytest.mark.unit decorator

src/tests/test_jit.py


3. CHANGELOG.md 📝 Documentation +1/-0

Document credential masking fix in changelog

• Added entry documenting the fix for credential leakage in GitHub Actions logs
• Notes that jit commands now register secrets with ::add-mask:: when GITHUB_ACTIONS=true
• Specifies which commands and fields are masked (aws, gcp, argo_wf, k8s, token)

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0)

Grey Divider


Remediation recommended

1. Unescaped mask command values 🐞 Bug ⛨ Security
Description
_mask_in_ci prints raw secret values into a single workflow-command line; if any value contains a
newline/carriage return, it will be emitted as multiple stderr lines so only the first line is a
valid "::add-mask::" command and the remainder can appear unmasked in logs. These values are sourced
from HTTP JSON responses (aws/gcp/argo/k8s), so the helper should normalize/split lines before
emitting commands.
Code

src/duplo_resource/jit.py[R37-41]

+  if os.environ.get("GITHUB_ACTIONS", "").lower() != "true":
+    return
+  for v in values:
+    if v:
+      print(f"::add-mask::{v}", file=sys.stderr, flush=True)
Evidence
The helper intentionally emits GitHub Actions workflow commands using print(f"::add-mask::{v}")
with unmodified v. The masked fields passed into this helper are obtained from remote HTTP
responses (self.client.get/post(...).json()), so control characters in those strings would be
printed verbatim and can break one-line workflow command emission.

src/duplo_resource/jit.py[21-42]
src/duplo_resource/jit.py[104-117]
src/duplo_resource/jit.py[144-157]
src/duplo_resource/jit.py[212-233]
src/duplo_resource/jit.py[463-495]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`_mask_in_ci()` prints `::add-mask::<value>` with the raw `value`. If `value` contains `\n`/`\r`, Python will emit multiple lines, which can result in only the first line being registered for masking and the remainder being written to logs without masking.

### Issue Context
The values passed to `_mask_in_ci()` come directly from portal API responses (`.json()`), so the CLI cannot safely assume they are always single-line strings.

### Fix Focus Areas
- src/duplo_resource/jit.py[21-42]
- src/tests/test_jit.py[9-55]

### What to change
- Coerce each value to `str`.
- Ensure each emitted workflow command is single-line by either:
 - iterating `for line in str(v).splitlines():` and emitting `::add-mask::` per non-empty line, or
 - explicitly replacing/removing `\r`/`\n` before printing.
- Add a unit test covering a value containing an embedded newline to ensure no raw continuation line is emitted.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@duploctl

duploctl Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
3790 1535 41% 0% 🟢

New Files

No new covered files...

Modified Files

File Coverage Status
src/duplo_resource/jit.py 22% 🟢
TOTAL 22% 🟢

updated for commit: c29ea91 by action🐍

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