Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions configs/promtail.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
server:

Check warning on line 1 in configs/promtail.yml

View workflow job for this annotation

GitHub Actions / Validate configs

1:1 [document-start] missing document start "---"
http_listen_port: 9080
grpc_listen_port: 0

Expand Down Expand Up @@ -36,6 +36,16 @@
- replace:
expression: '(?i)(password[=:]\s*)\S+'
replace: '${1}<redacted>'
# URL-embedded credentials: scheme://user:password@host -> scheme://<redacted>@host
- replace:
expression: '([a-zA-Z][a-zA-Z0-9+.\-]*://)[^\s/@:]+:[^\s/@]+@'
replace: '${1}<redacted>@'
# Query-string API keys / tokens / secrets / passwords:
# ?api_key=abc, &access_token=xyz, &client_secret=..., &password=...
- replace:
# yamllint disable-line rule:line-length
expression: '(?i)([?&](?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret|token|password|key)=)[^&\s#]+'
replace: '${1}<redacted>'

# Tail Hermes application log
- job_name: hermes
Expand All @@ -62,6 +72,16 @@
- replace:
expression: '(?i)(password[=:]\s*)\S+'
replace: '${1}<redacted>'
# URL-embedded credentials: scheme://user:password@host -> scheme://<redacted>@host
- replace:
expression: '([a-zA-Z][a-zA-Z0-9+.\-]*://)[^\s/@:]+:[^\s/@]+@'
replace: '${1}<redacted>@'
# Query-string API keys / tokens / secrets / passwords:
# ?api_key=abc, &access_token=xyz, &client_secret=..., &password=...
- replace:
# yamllint disable-line rule:line-length
expression: '(?i)([?&](?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret|token|password|key)=)[^&\s#]+'
replace: '${1}<redacted>'

# Tail NATS server log
- job_name: nats
Expand All @@ -88,3 +108,13 @@
- replace:
expression: '(?i)(password[=:]\s*)\S+'
replace: '${1}<redacted>'
# URL-embedded credentials: scheme://user:password@host -> scheme://<redacted>@host
- replace:
expression: '([a-zA-Z][a-zA-Z0-9+.\-]*://)[^\s/@:]+:[^\s/@]+@'
replace: '${1}<redacted>@'
# Query-string API keys / tokens / secrets / passwords:
# ?api_key=abc, &access_token=xyz, &client_secret=..., &password=...
- replace:
# yamllint disable-line rule:line-length
expression: '(?i)([?&](?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret|token|password|key)=)[^&\s#]+'
replace: '${1}<redacted>'
65 changes: 65 additions & 0 deletions tests/test_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,71 @@ def test_redaction_enabled_jobs_have_secret_patterns(self):
f"{pat!r}; got expressions: {joined!r}"
)

def _replace_expressions(self, job_name: str) -> list[str]:
job = next(
(j for j in self.config["scrape_configs"] if j.get("job_name") == job_name),
None,
)
assert job is not None, f"{job_name} scrape job not found"
stages = job.get("pipeline_stages", [])
exprs: list[str] = []
for stage in stages:
if isinstance(stage, dict) and "replace" in stage:
expr = stage["replace"].get("expression")
if expr:
exprs.append(expr)
return exprs

def _jobs_with_redaction(self) -> list[str]:
"""Return job_names that already perform credential redaction."""
jobs: list[str] = []
for j in self.config["scrape_configs"]:
stages = j.get("pipeline_stages") or []
if any(isinstance(s, dict) and "replace" in s for s in stages):
jobs.append(j["job_name"])
return jobs

def test_url_embedded_credentials_redacted_in_all_redaction_pipelines(self):
"""Issue #194: scheme://user:password@host must be redacted in every
pipeline that already does credential redaction."""
import re

jobs = self._jobs_with_redaction()
assert jobs, "expected at least one job with redaction stages"
sample_in = "connecting to https://alice:hunter2@db.example.com/foo"
for job in jobs:
sample = sample_in
for expr in self._replace_expressions(job):
sample = re.sub(expr, "<redacted>", sample, flags=re.IGNORECASE)
assert "hunter2" not in sample, (
f"job {job!r}: URL-embedded password leaked: {sample!r}"
)
assert "alice" not in sample, (
f"job {job!r}: URL-embedded username leaked: {sample!r}"
)

def test_query_string_api_keys_redacted_in_all_redaction_pipelines(self):
"""Issue #194: ?api_key= / &access_token= etc must be redacted in every
pipeline that already does credential redaction."""
import re

jobs = self._jobs_with_redaction()
cases = [
("GET /v1?api_key=abc123XYZ&name=alice", "abc123XYZ"),
("url?access_token=tok_DEADBEEF&next=x", "tok_DEADBEEF"),
("/auth?client_secret=shh_SECRET_42 done", "shh_SECRET_42"),
]
for job in jobs:
exprs = self._replace_expressions(job)
for line, secret in cases:
redacted = line
for expr in exprs:
redacted = re.sub(expr, "<redacted>", redacted, flags=re.IGNORECASE)
assert secret not in redacted, (
f"job {job!r}: query-string secret {secret!r} leaked: "
f"input={line!r} output={redacted!r}"
)


class TestGrafanaDatasourcesConfig(unittest.TestCase):
def setUp(self):
Expand Down
Loading