From e84684e2c7d88cb155759738821e3fb7e388cb45 Mon Sep 17 00:00:00 2001 From: asmhatre Date: Mon, 26 Oct 2026 01:43:28 +0000 Subject: [PATCH] Add redaction module: JFR in-process data redaction (JEP 536) on JDK 27 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CifR2BE7PBvRy5hT8TS9oz --- README.md | 7 + redaction/.gitignore | 1 + redaction/README.md | 44 +++++ redaction/filters/args.txt | 3 + redaction/filters/keys.txt | 2 + redaction/output/01-jdk25-leak.txt | 163 ++++++++++++++++++ redaction/output/02-jdk27-defaults.txt | 163 ++++++++++++++++++ redaction/output/03-jdk27-opt-out.txt | 9 + redaction/output/04-jdk27-custom-inline.txt | 46 +++++ .../05-jdk27-filter-files-and-debug-log.txt | 28 +++ .../output/06-jdk27-missing-filter-file.txt | 8 + .../07-jdk27-no-plus-replaces-defaults.txt | 12 ++ .../output/08-jdk27-jcmd-late-recording.txt | 17 ++ redaction/scripts/common.sh | 45 +++++ redaction/scripts/run-all.sh | 82 +++++++++ redaction/src/Scan.java | 35 ++++ redaction/src/Workload.java | 17 ++ 17 files changed, 682 insertions(+) create mode 100644 redaction/.gitignore create mode 100644 redaction/README.md create mode 100644 redaction/filters/args.txt create mode 100644 redaction/filters/keys.txt create mode 100644 redaction/output/01-jdk25-leak.txt create mode 100644 redaction/output/02-jdk27-defaults.txt create mode 100644 redaction/output/03-jdk27-opt-out.txt create mode 100644 redaction/output/04-jdk27-custom-inline.txt create mode 100644 redaction/output/05-jdk27-filter-files-and-debug-log.txt create mode 100644 redaction/output/06-jdk27-missing-filter-file.txt create mode 100644 redaction/output/07-jdk27-no-plus-replaces-defaults.txt create mode 100644 redaction/output/08-jdk27-jcmd-late-recording.txt create mode 100755 redaction/scripts/common.sh create mode 100755 redaction/scripts/run-all.sh create mode 100644 redaction/src/Scan.java create mode 100644 redaction/src/Workload.java diff --git a/README.md b/README.md index 0572500..a280163 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,13 @@ Every number in the post comes from a file under `docs/output/`, produced by either the JUnit test suite or a real `jcmd` + `curl` session against a running instance of this app — see the index below. +## Modules + +| Module | What it is | Post | +|---|---|---| +| (repo root) | Spring Boot app: profiling a live app with JFR and Mission Control | [Java Flight Recorder and Mission Control](https://ankurm.com/java-flight-recorder-mission-control-spring-boot-profiling/) | +| [`redaction/`](redaction/) | JDK 27 JEP 536: keep secrets out of recordings, with a leak scanner | [JFR In-Process Data Redaction](https://ankurm.com/jfr-data-redaction-jep-536-safe-flight-recordings-in-production/) | + ## Versions this was built and verified against | Component | Version | Verified against | diff --git a/redaction/.gitignore b/redaction/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/redaction/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/redaction/README.md b/redaction/README.md new file mode 100644 index 0000000..8cf059e --- /dev/null +++ b/redaction/README.md @@ -0,0 +1,44 @@ +# redaction — JFR In-Process Data Redaction (JEP 536) + +Companion module for the ankurm.com post +**"JFR In-Process Data Redaction (JEP 536): Safe Flight Recordings in Production"** +(https://ankurm.com/jfr-data-redaction-jep-536-safe-flight-recordings-in-production/). +It extends the JFR / Mission Control post's workflow with the JDK 27 feature that +removes secrets from recordings before they are written. + +No Maven and no dependencies: two small Java files and shell scripts. + +| Component | Version | Verified against | +|---|---|---| +| JDK (redaction runs) | Temurin 27.0.1.0.2+1 | `java -version`; JEP 536 status "Closed / Delivered", release 27 | +| JDK (leak baseline) | Temurin 25.0.5+8 | `java -version`; JDK 25 has no redaction | + +## Layout + +| Path | What it is | +|---|---| +| `src/Workload.java` | Trivial program; the secrets are on its command line, `-D` flags and environment | +| `src/Scan.java` | Checks a `.jfr` file for planted secrets, by raw bytes and by decoded events | +| `filters/keys.txt`, `filters/args.txt` | Filter files for `redact-key=@...` / `redact-argument=@...` | +| `scripts/common.sh` | Plants seven fake secrets under `env -i` (your real environment is never passed in) | +| `scripts/run-all.sh` | Regenerates every file in `output/` | +| `output/*.txt` | Captured real output, quoted verbatim in the post | + +## Run it + +```bash +JDK27=/path/to/jdk-27 JDK25=/path/to/jdk-25 redaction/scripts/run-all.sh +``` + +## Output index + +| File | What it shows | +|---|---| +| `01-jdk25-leak.txt` | JDK 25: all seven secrets sit in the recording | +| `02-jdk27-defaults.txt` | JDK 27, no flags: three secrets redacted, four still leak | +| `03-jdk27-opt-out.txt` | `redact-argument=none,redact-key=none` brings every leak back | +| `04-jdk27-custom-inline.txt` | `conn*` fails to match `DB_CONN` (globs are anchored); `*conn*` fixes it | +| `05-jdk27-filter-files-and-debug-log.txt` | Filter files with `@`, and `-Xlog:jfr+redact=debug` | +| `06-jdk27-missing-filter-file.txt` | A missing filter file stops the JVM at startup | +| `07-jdk27-no-plus-replaces-defaults.txt` | Without `+`, your list replaces the defaults | +| `08-jdk27-jcmd-late-recording.txt` | A recording started later with `jcmd JFR.start` is redacted the same way | diff --git a/redaction/filters/args.txt b/redaction/filters/args.txt new file mode 100644 index 0000000..bb3b2b0 --- /dev/null +++ b/redaction/filters/args.txt @@ -0,0 +1,3 @@ +--db-pass * +--connection-string * +https://*:*@* diff --git a/redaction/filters/keys.txt b/redaction/filters/keys.txt new file mode 100644 index 0000000..6fbef09 --- /dev/null +++ b/redaction/filters/keys.txt @@ -0,0 +1,2 @@ +*db-pass* +*conn* diff --git a/redaction/output/01-jdk25-leak.txt b/redaction/output/01-jdk25-leak.txt new file mode 100644 index 0000000..bd2a38a --- /dev/null +++ b/redaction/output/01-jdk25-leak.txt @@ -0,0 +1,163 @@ +### JDK 25: no redaction exists. What is in the recording? +openjdk version "25.0.5" 2026-10-20 LTS +OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS) +OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing) +LEAK-ENV-TOKEN-1 RAW=LEAK EVENTS=1 +LEAK-PROP-PASSWORD-2 RAW=LEAK EVENTS=3 +LEAK-ARG-DBPASSWORD-3 RAW=LEAK EVENTS=3 +LEAK-URL-PW-4 RAW=LEAK EVENTS=3 +LEAK-ENV-CONN-5 RAW=LEAK EVENTS=1 +LEAK-PROP-DBPASS-6 RAW=LEAK EVENTS=3 +LEAK-ARG-DBPASS-7 RAW=LEAK EVENTS=3 +exit=2 + +### jfr print +jdk.JVMInformation { + startTime = 01:42:36.460 (2026-10-26) + jvmName = "OpenJDK 64-Bit Server VM" + jvmVersion = "OpenJDK 64-Bit Server VM (25.0.5+8-LTS) for linux-amd64 JRE (25.0.5+8-LTS), built on 2026-10-20T00:00:00Z with gcc 14.2.0" + jvmArguments = "-XX:StartFlightRecording:filename=/tmp/jfr-redaction/jdk25.jfr -Xmx256m -Djavax.net.ssl.keyStorePassword=LEAK-PROP-PASSWORD-2 -Ddb-pass=LEAK-PROP-DBPASS-6 -Dapp.region=mumbai" + jvmFlags = N/A + javaArguments = "Workload --dbpassword LEAK-ARG-DBPASSWORD-3 --db-pass LEAK-ARG-DBPASS-7 https://john:LEAK-URL-PW-4@example.com/login --verbose" + jvmStartTime = 01:42:35.982 (2026-10-26) + pid = 6378 +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.vm.specification.name" + value = "Java Virtual Machine Specification" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.vm.name" + value = "OpenJDK 64-Bit Server VM" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.home" + value = "/tmp/claude-0/post2/j25/jdk-25.0.5+8" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "app.region" + value = "mumbai" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "sun.java.command" + value = "Workload --dbpassword LEAK-ARG-DBPASSWORD-3 --db-pass LEAK-ARG-DBPASS-7 https://john:LEAK-URL-PW-4@example.com/login --verbose" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.class.path" + value = "/tmp/claude-0/post2/repo/redaction/build" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "sun.java.launcher" + value = "SUN_STANDARD" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.vm.compressedOopsMode" + value = "32-bit" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "jdk.debug" + value = "release" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "db-pass" + value = "LEAK-PROP-DBPASS-6" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "javax.net.ssl.keyStorePassword" + value = "LEAK-PROP-PASSWORD-2" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.vm.vendor" + value = "Eclipse Adoptium" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.vm.specification.version" + value = "25" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.vm.info" + value = "mixed mode, sharing" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.vm.specification.vendor" + value = "Oracle Corporation" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.vm.version" + value = "25.0.5+8-LTS" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "sun.boot.library.path" + value = "/tmp/claude-0/post2/j25/jdk-25.0.5+8/lib" +} + +jdk.InitialSystemProperty { + startTime = 01:42:36.460 (2026-10-26) + key = "java.library.path" + value = "/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:36.460 (2026-10-26) + key = "PATH" + value = "/usr/bin:/bin" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:36.460 (2026-10-26) + key = "DEPLOY_REGION" + value = "eu-west-1" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:36.460 (2026-10-26) + key = "HOME" + value = "/nonexistent" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:36.460 (2026-10-26) + key = "ACCESS_TOKEN" + value = "LEAK-ENV-TOKEN-1" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:36.460 (2026-10-26) + key = "DB_CONN" + value = "LEAK-ENV-CONN-5" +} + diff --git a/redaction/output/02-jdk27-defaults.txt b/redaction/output/02-jdk27-defaults.txt new file mode 100644 index 0000000..7baeadc --- /dev/null +++ b/redaction/output/02-jdk27-defaults.txt @@ -0,0 +1,163 @@ +### JDK 27 with NO redaction flags at all +openjdk version "27.0.1.0.2" 2026-10-21 +OpenJDK Runtime Environment Temurin-27.0.1.0.2+1 (build 27.0.1.0.2+1) +OpenJDK 64-Bit Server VM Temurin-27.0.1.0.2+1 (build 27.0.1.0.2+1, mixed mode, sharing) +LEAK-ENV-TOKEN-1 RAW=clean EVENTS=0 +LEAK-PROP-PASSWORD-2 RAW=clean EVENTS=0 +LEAK-ARG-DBPASSWORD-3 RAW=clean EVENTS=0 +LEAK-URL-PW-4 RAW=LEAK EVENTS=2 +LEAK-ENV-CONN-5 RAW=LEAK EVENTS=1 +LEAK-PROP-DBPASS-6 RAW=LEAK EVENTS=2 +LEAK-ARG-DBPASS-7 RAW=LEAK EVENTS=2 +exit=2 + +### jfr print +jdk.JVMInformation { + startTime = 01:42:38.133 (2026-10-26) + jvmName = "OpenJDK 64-Bit Server VM" + jvmVersion = "OpenJDK 64-Bit Server VM (27.0.1.0.2+1) for linux-amd64 JRE (27.0.1.0.2+1), built on 2026-10-21T00:00:00Z with gcc 14.2.0" + jvmArguments = "-XX:StartFlightRecording:filename=/tmp/jfr-redaction/default.jfr -Xmx256m [REDACTED] -Ddb-pass=LEAK-PROP-DBPASS-6 -Dapp.region=mumbai" + jvmFlags = N/A + javaArguments = "Workload [REDACTED] [REDACTED] --db-pass LEAK-ARG-DBPASS-7 https://john:LEAK-URL-PW-4@example.com/login --verbose" + jvmStartTime = 01:42:37.505 (2026-10-26) + pid = 6463 +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.vm.specification.name" + value = "Java Virtual Machine Specification" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.vm.name" + value = "OpenJDK 64-Bit Server VM" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.home" + value = "/tmp/claude-0/post2/jdk-27.0.1.0.2+1" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "app.region" + value = "mumbai" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "sun.java.command" + value = "Workload [REDACTED] [REDACTED] --db-pass LEAK-ARG-DBPASS-7 https://john:LEAK-URL-PW-4@example.com/login --verbose" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.class.path" + value = "/tmp/claude-0/post2/repo/redaction/build" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "sun.java.launcher" + value = "SUN_STANDARD" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.vm.compressedOopsMode" + value = "32-bit" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "jdk.debug" + value = "release" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "db-pass" + value = "LEAK-PROP-DBPASS-6" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "javax.net.ssl.keyStorePassword" + value = "[REDACTED]" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.vm.vendor" + value = "Eclipse Adoptium" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.vm.specification.version" + value = "27" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.vm.info" + value = "mixed mode, sharing" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.vm.specification.vendor" + value = "Oracle Corporation" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.vm.version" + value = "27.0.1.0.2+1" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "sun.boot.library.path" + value = "/tmp/claude-0/post2/jdk-27.0.1.0.2+1/lib" +} + +jdk.InitialSystemProperty { + startTime = 01:42:38.133 (2026-10-26) + key = "java.library.path" + value = "/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:38.133 (2026-10-26) + key = "PATH" + value = "/usr/bin:/bin" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:38.133 (2026-10-26) + key = "DEPLOY_REGION" + value = "eu-west-1" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:38.133 (2026-10-26) + key = "HOME" + value = "/nonexistent" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:38.133 (2026-10-26) + key = "ACCESS_TOKEN" + value = "[REDACTED]" +} + +jdk.InitialEnvironmentVariable { + startTime = 01:42:38.133 (2026-10-26) + key = "DB_CONN" + value = "LEAK-ENV-CONN-5" +} + diff --git a/redaction/output/03-jdk27-opt-out.txt b/redaction/output/03-jdk27-opt-out.txt new file mode 100644 index 0000000..fd7b48d --- /dev/null +++ b/redaction/output/03-jdk27-opt-out.txt @@ -0,0 +1,9 @@ +### JDK 27 with redact-argument=none,redact-key=none (the opt-out) +LEAK-ENV-TOKEN-1 RAW=LEAK EVENTS=1 +LEAK-PROP-PASSWORD-2 RAW=LEAK EVENTS=2 +LEAK-ARG-DBPASSWORD-3 RAW=LEAK EVENTS=2 +LEAK-URL-PW-4 RAW=LEAK EVENTS=2 +LEAK-ENV-CONN-5 RAW=LEAK EVENTS=1 +LEAK-PROP-DBPASS-6 RAW=LEAK EVENTS=2 +LEAK-ARG-DBPASS-7 RAW=LEAK EVENTS=2 +exit=2 diff --git a/redaction/output/04-jdk27-custom-inline.txt b/redaction/output/04-jdk27-custom-inline.txt new file mode 100644 index 0000000..9a4f220 --- /dev/null +++ b/redaction/output/04-jdk27-custom-inline.txt @@ -0,0 +1,46 @@ +### JDK 27, first attempt: conn* and a +prefix +flags: -XX:FlightRecorderOptions:redact-key=+db-pass;conn*,redact-argument=+--db-pass *;https://*:*@* +LEAK-ENV-TOKEN-1 RAW=clean EVENTS=0 +LEAK-PROP-PASSWORD-2 RAW=clean EVENTS=0 +LEAK-ARG-DBPASSWORD-3 RAW=clean EVENTS=0 +LEAK-URL-PW-4 RAW=clean EVENTS=0 +LEAK-ENV-CONN-5 RAW=LEAK EVENTS=1 +LEAK-PROP-DBPASS-6 RAW=clean EVENTS=0 +LEAK-ARG-DBPASS-7 RAW=clean EVENTS=0 +exit=2 + +### jfr print (JVMInformation and the DB_CONN variable) +jdk.JVMInformation { + startTime = 01:42:44.988 (2026-10-26) + jvmName = "OpenJDK 64-Bit Server VM" + jvmVersion = "OpenJDK 64-Bit Server VM (27.0.1.0.2+1) for linux-amd64 JRE (27.0.1.0.2+1), built on 2026-10-21T00:00:00Z with gcc 14.2.0" + jvmArguments = "-XX:StartFlightRecording:filename=/tmp/jfr-redaction/wrongglob.jfr -XX:FlightRecorderOptions:redact-key=+db-pass;conn*,redact-argument=[REDACTED] -Xmx256m [REDACTED] -Ddb-pass=[REDACTED] -Dapp.region=mumbai" + jvmFlags = N/A + javaArguments = "Workload [REDACTED] [REDACTED] [REDACTED] [REDACTED] [REDACTED] --verbose" + jvmStartTime = 01:42:43.787 (2026-10-26) + pid = 6681 +} + key = "DB_CONN" + value = "LEAK-ENV-CONN-5" +### fixed: *conn* instead of conn* +flags: -XX:FlightRecorderOptions:redact-key=+db-pass;*conn*,redact-argument=+--db-pass *;https://*:*@* +LEAK-ENV-TOKEN-1 RAW=clean EVENTS=0 +LEAK-PROP-PASSWORD-2 RAW=clean EVENTS=0 +LEAK-ARG-DBPASSWORD-3 RAW=clean EVENTS=0 +LEAK-URL-PW-4 RAW=clean EVENTS=0 +LEAK-ENV-CONN-5 RAW=clean EVENTS=0 +LEAK-PROP-DBPASS-6 RAW=clean EVENTS=0 +LEAK-ARG-DBPASS-7 RAW=clean EVENTS=0 +exit=0 + +### jfr print (JVMInformation) +jdk.JVMInformation { + startTime = 01:42:49.421 (2026-10-26) + jvmName = "OpenJDK 64-Bit Server VM" + jvmVersion = "OpenJDK 64-Bit Server VM (27.0.1.0.2+1) for linux-amd64 JRE (27.0.1.0.2+1), built on 2026-10-21T00:00:00Z with gcc 14.2.0" + jvmArguments = "-XX:StartFlightRecording:filename=/tmp/jfr-redaction/fixed.jfr -XX:FlightRecorderOptions:redact-key=+db-pass;*conn*,redact-argument=[REDACTED] -Xmx256m [REDACTED] -Ddb-pass=[REDACTED] -Dapp.region=mumbai" + jvmFlags = N/A + javaArguments = "Workload [REDACTED] [REDACTED] [REDACTED] [REDACTED] [REDACTED] --verbose" + jvmStartTime = 01:42:47.483 (2026-10-26) + pid = 6823 +} diff --git a/redaction/output/05-jdk27-filter-files-and-debug-log.txt b/redaction/output/05-jdk27-filter-files-and-debug-log.txt new file mode 100644 index 0000000..b88eb2d --- /dev/null +++ b/redaction/output/05-jdk27-filter-files-and-debug-log.txt @@ -0,0 +1,28 @@ +### JDK 27, filters loaded from files with @, plus -Xlog:jfr+redact=debug +keys.txt: + *db-pass* + *conn* +args.txt: + --db-pass * + --connection-string * + https://*:*@* + +[0.153s][debug][jfr,redact] Redaction file /filters/args.txt read successfully +[0.156s][debug][jfr,redact] Redaction file /filters/keys.txt read successfully +[0.157s][debug][jfr,redact] Redacted initial environment variable named 'ACCESS_TOKEN' +[0.157s][debug][jfr,redact] Redacted initial environment variable named 'DB_CONN' +[0.157s][debug][jfr,redact] Redacted initial system property named 'javax.net.ssl.keyStorePassword' +[0.157s][debug][jfr,redact] Redacted initial system property named 'db-pass' +[0.157s][debug][jfr,redact] Redacted initial system property named 'sun.java.command' +[0.157s][debug][jfr,redact] Redacted Java Arguments 'Workload [REDACTED] [REDACTED] [REDACTED] [REDACTED] [REDACTED] --verbose' +[0.157s][debug][jfr,redact] Redacted JVM Arguments '-XX:StartFlightRecording:filename=/tmp/jfr-redaction/files.jfr -XX:FlightRecorderOptions:redact-key=+@/filters/keys.txt,redact-argument=[REDACTED] -Xlog:jfr+redact=debug -Xmx256m [REDACTED] -Ddb-pass=[REDACTED] -Dapp.region=mumbai' +[0.157s][debug][jfr,redact] Redacted string flag 'FlightRecorderOptions' + +LEAK-ENV-TOKEN-1 RAW=clean EVENTS=0 +LEAK-PROP-PASSWORD-2 RAW=clean EVENTS=0 +LEAK-ARG-DBPASSWORD-3 RAW=clean EVENTS=0 +LEAK-URL-PW-4 RAW=clean EVENTS=0 +LEAK-ENV-CONN-5 RAW=clean EVENTS=0 +LEAK-PROP-DBPASS-6 RAW=clean EVENTS=0 +LEAK-ARG-DBPASS-7 RAW=clean EVENTS=0 +exit=0 diff --git a/redaction/output/06-jdk27-missing-filter-file.txt b/redaction/output/06-jdk27-missing-filter-file.txt new file mode 100644 index 0000000..964f2e3 --- /dev/null +++ b/redaction/output/06-jdk27-missing-filter-file.txt @@ -0,0 +1,8 @@ +### JDK 27, a filter file that does not exist +[0.105s][warning][jfr,redact] Default redaction filters are replaced. Prepend with '+' to add filters to the +[0.105s][warning][jfr,redact] defaults, or specify -XX:FlightRecorderOptions:'redact-key=none;' +[0.105s][warning][jfr,redact] to replace without a warning. +[0.105s][error ][jfr,redact] Failed to access redaction file /no/such/keys.txt +Error occurred during initialization of VM +Failure when starting JFR on_create_vm_2 +exit=1 diff --git a/redaction/output/07-jdk27-no-plus-replaces-defaults.txt b/redaction/output/07-jdk27-no-plus-replaces-defaults.txt new file mode 100644 index 0000000..2dd833a --- /dev/null +++ b/redaction/output/07-jdk27-no-plus-replaces-defaults.txt @@ -0,0 +1,12 @@ +### JDK 27, redact-key=db-pass WITHOUT the + prefix replaces the defaults +[0.127s][warning][jfr,redact] Default redaction filters are replaced. Prepend with '+' to add filters to the +[0.127s][warning][jfr,redact] defaults, or specify -XX:FlightRecorderOptions:'redact-key=none;' +[0.127s][warning][jfr,redact] to replace without a warning. +LEAK-ENV-TOKEN-1 RAW=LEAK EVENTS=1 +LEAK-PROP-PASSWORD-2 RAW=LEAK EVENTS=1 +LEAK-ARG-DBPASSWORD-3 RAW=clean EVENTS=0 +LEAK-URL-PW-4 RAW=LEAK EVENTS=2 +LEAK-ENV-CONN-5 RAW=LEAK EVENTS=1 +LEAK-PROP-DBPASS-6 RAW=clean EVENTS=0 +LEAK-ARG-DBPASS-7 RAW=LEAK EVENTS=2 +exit=2 diff --git a/redaction/output/08-jdk27-jcmd-late-recording.txt b/redaction/output/08-jdk27-jcmd-late-recording.txt new file mode 100644 index 0000000..a19e781 --- /dev/null +++ b/redaction/output/08-jdk27-jcmd-late-recording.txt @@ -0,0 +1,17 @@ +### JDK 27, recording started later with jcmd JFR.start (JVM launched without any JFR flag) +7047: +Started recording 1. No limit specified, using maxsize=250MB as default. + +Use jcmd 7047 JFR.dump name=late to copy recording data to file. +7047: +Stopped recording "late", 206.8 kB written to: + +/late.jfr +LEAK-ENV-TOKEN-1 RAW=clean EVENTS=0 +LEAK-PROP-PASSWORD-2 RAW=clean EVENTS=0 +LEAK-ARG-DBPASSWORD-3 RAW=clean EVENTS=0 +LEAK-URL-PW-4 RAW=LEAK EVENTS=2 +LEAK-ENV-CONN-5 RAW=LEAK EVENTS=1 +LEAK-PROP-DBPASS-6 RAW=LEAK EVENTS=2 +LEAK-ARG-DBPASS-7 RAW=LEAK EVENTS=2 +exit=2 diff --git a/redaction/scripts/common.sh b/redaction/scripts/common.sh new file mode 100755 index 0000000..aa877e6 --- /dev/null +++ b/redaction/scripts/common.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Shared helpers. Source this; do not run it. +# +# run_case +# Runs the Workload with the SAME planted secrets every time, under a clean +# environment (env -i), so only the fake secrets below can ever end up in a +# recording. Your real shell environment is never passed through. +HERE="$(cd "$(dirname "$0")/.." && pwd)" +BUILD="$HERE/build" + +# The planted secrets. Each one has a name that tells you where it hides. +SECRETS=(LEAK-ENV-TOKEN-1 LEAK-PROP-PASSWORD-2 LEAK-ARG-DBPASSWORD-3 LEAK-URL-PW-4 + LEAK-ENV-CONN-5 LEAK-PROP-DBPASS-6 LEAK-ARG-DBPASS-7) + +compile() { # + mkdir -p "$BUILD" + "$1/bin/javac" --release 21 -d "$BUILD" "$HERE/src/Workload.java" "$HERE/src/Scan.java" 2>&1 | grep -v '^Picked up' || true +} + +run_jvm() { # [flags...] (no recording flag: caller decides) + local jdk="$1"; shift + env -i PATH=/usr/bin:/bin HOME=/nonexistent \ + ACCESS_TOKEN=LEAK-ENV-TOKEN-1 DB_CONN=LEAK-ENV-CONN-5 DEPLOY_REGION=eu-west-1 \ + "$jdk/bin/java" "$@" \ + -Xmx256m \ + -Djavax.net.ssl.keyStorePassword=LEAK-PROP-PASSWORD-2 \ + -Ddb-pass=LEAK-PROP-DBPASS-6 \ + -Dapp.region=mumbai \ + -cp "$BUILD" Workload \ + --dbpassword LEAK-ARG-DBPASSWORD-3 \ + --db-pass LEAK-ARG-DBPASS-7 \ + https://john:LEAK-URL-PW-4@example.com/login \ + --verbose +} + +run_case() { # [flags...] (recording from JVM start) + local jdk="$1" out="$2"; shift 2 + run_jvm "$jdk" -XX:StartFlightRecording:filename="$out" "$@" +} + +scan() { "$1/bin/java" -cp "$BUILD" Scan "$2" "${SECRETS[@]}" 2>&1 | grep -v '^Picked up'; } + +print_events() { # + "$1/bin/jfr" print --events InitialSystemProperty,JVMInformation,InitialEnvironmentVariable "$2" 2>&1 | grep -v '^Picked up' +} diff --git a/redaction/scripts/run-all.sh b/redaction/scripts/run-all.sh new file mode 100755 index 0000000..d3aad98 --- /dev/null +++ b/redaction/scripts/run-all.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Regenerates every file under redaction/output/. +# Usage: JDK27=/path/to/jdk-27 JDK25=/path/to/jdk-25 redaction/scripts/run-all.sh +# Each case plants the same seven fake secrets (see common.sh), records, then +# greps the recording for them. Recordings go to a scratch dir, not the repo. +set -uo pipefail +source "$(dirname "$0")/common.sh" +: "${JDK27:?set JDK27 to a JDK 27 home}" "${JDK25:?set JDK25 to a JDK 25 home}" +OUT="$HERE/output"; TMP=/tmp/jfr-redaction; rm -rf "$TMP"; mkdir -p "$OUT" "$TMP" +cd "$HERE" +unset JAVA_TOOL_OPTIONS # keep proxy banners out of committed output +compile "$JDK27" +vers() { "$1/bin/java" -version 2>&1 | grep -v '^Picked up'; } +hdr() { echo "### $*"; } + +{ hdr "JDK 25: no redaction exists. What is in the recording?" + vers "$JDK25" + run_case "$JDK25" "$TMP/jdk25.jfr" >/dev/null + scan "$JDK25" "$TMP/jdk25.jfr"; echo "exit=${PIPESTATUS[0]}" + echo; hdr "jfr print"; print_events "$JDK25" "$TMP/jdk25.jfr" +} > "$OUT/01-jdk25-leak.txt" 2>&1 + +{ hdr "JDK 27 with NO redaction flags at all" + vers "$JDK27" + run_case "$JDK27" "$TMP/default.jfr" >/dev/null + scan "$JDK27" "$TMP/default.jfr"; echo "exit=${PIPESTATUS[0]}" + echo; hdr "jfr print"; print_events "$JDK27" "$TMP/default.jfr" +} > "$OUT/02-jdk27-defaults.txt" 2>&1 + +{ hdr "JDK 27 with redact-argument=none,redact-key=none (the opt-out)" + run_case "$JDK27" "$TMP/none.jfr" -XX:FlightRecorderOptions:redact-argument=none,redact-key=none >/dev/null + scan "$JDK27" "$TMP/none.jfr"; echo "exit=${PIPESTATUS[0]}" +} > "$OUT/03-jdk27-opt-out.txt" 2>&1 + +{ hdr "JDK 27, first attempt: conn* and a +prefix" + F="-XX:FlightRecorderOptions:redact-key=+db-pass;conn*,redact-argument=+--db-pass *;https://*:*@*" + echo "flags: $F" + run_case "$JDK27" "$TMP/wrongglob.jfr" "$F" >/dev/null + scan "$JDK27" "$TMP/wrongglob.jfr"; echo "exit=${PIPESTATUS[0]}" + echo; hdr "jfr print (JVMInformation and the DB_CONN variable)" + print_events "$JDK27" "$TMP/wrongglob.jfr" | awk '/jdk.JVMInformation/,/^}/ {print} /key = "DB_CONN"/ {print; getline; print}' + hdr "fixed: *conn* instead of conn*" + F="-XX:FlightRecorderOptions:redact-key=+db-pass;*conn*,redact-argument=+--db-pass *;https://*:*@*" + echo "flags: $F" + run_case "$JDK27" "$TMP/fixed.jfr" "$F" >/dev/null + scan "$JDK27" "$TMP/fixed.jfr"; echo "exit=${PIPESTATUS[0]}" + echo; hdr "jfr print (JVMInformation)" + print_events "$JDK27" "$TMP/fixed.jfr" | awk '/jdk.JVMInformation/,/^}/ {print}' +} > "$OUT/04-jdk27-custom-inline.txt" 2>&1 + +{ hdr "JDK 27, filters loaded from files with @, plus -Xlog:jfr+redact=debug" + echo "keys.txt:"; sed 's/^/ /' "$HERE/filters/keys.txt" + echo "args.txt:"; sed 's/^/ /' "$HERE/filters/args.txt" + echo + F="-XX:FlightRecorderOptions:redact-key=+@$HERE/filters/keys.txt,redact-argument=+@$HERE/filters/args.txt" + run_case "$JDK27" "$TMP/files.jfr" "$F" -Xlog:jfr+redact=debug 2>&1 \ + | grep -E 'jfr,redact' | sed "s#$HERE##g" + echo; scan "$JDK27" "$TMP/files.jfr"; echo "exit=${PIPESTATUS[0]}" +} > "$OUT/05-jdk27-filter-files-and-debug-log.txt" 2>&1 + +{ hdr "JDK 27, a filter file that does not exist" + env -i PATH=/usr/bin:/bin "$JDK27/bin/java" -XX:FlightRecorderOptions:redact-key=@/no/such/keys.txt -version 2>&1 | grep -v '^Picked up' + echo "exit=${PIPESTATUS[0]}" +} > "$OUT/06-jdk27-missing-filter-file.txt" 2>&1 + +{ hdr "JDK 27, redact-key=db-pass WITHOUT the + prefix replaces the defaults" + run_case "$JDK27" "$TMP/noplus.jfr" -XX:FlightRecorderOptions:redact-key=db-pass 2>&1 | grep -E 'jfr,redact' + scan "$JDK27" "$TMP/noplus.jfr"; echo "exit=${PIPESTATUS[0]}" +} > "$OUT/07-jdk27-no-plus-replaces-defaults.txt" 2>&1 + +{ hdr "JDK 27, recording started later with jcmd JFR.start (JVM launched without any JFR flag)" + run_jvm "$JDK27" -Dworkload.sleep.ms=8000 >/dev/null & + sleep 2 + PID=$(pgrep -f 'Workload' | head -1) + "$JDK27/bin/jcmd" "$PID" JFR.start name=late filename="$TMP/late.jfr" 2>&1 | grep -v '^Picked up' | sed "s#$TMP##g" + sleep 1 + "$JDK27/bin/jcmd" "$PID" JFR.stop name=late 2>&1 | grep -v '^Picked up' | sed "s#$TMP##g" + wait + scan "$JDK27" "$TMP/late.jfr"; echo "exit=${PIPESTATUS[0]}" +} > "$OUT/08-jdk27-jcmd-late-recording.txt" 2>&1 + +echo "regenerated:"; ls -1 "$OUT" diff --git a/redaction/src/Scan.java b/redaction/src/Scan.java new file mode 100644 index 0000000..d566c3f --- /dev/null +++ b/redaction/src/Scan.java @@ -0,0 +1,35 @@ +import java.nio.file.*; +import java.util.*; +import jdk.jfr.consumer.*; + +/** + * Answers one question about a .jfr file: do the planted secrets appear in it? + * + * Two independent checks, because they fail differently: + * RAW - searches the raw bytes of the file for each secret (what an attacker with + * `strings` or a hex editor would do). + * EVENT - searches every field of every event after decoding (what `jfr print` shows). + * + * Usage: java Scan.java recording.jfr secret1 secret2 ... + * Prints one line per secret and exits 0 if none leaked, 2 if any leaked. + */ +public class Scan { + public static void main(String[] a) throws Exception { + Path file = Path.of(a[0]); + byte[] raw = Files.readAllBytes(file); + String rawLatin = new String(raw, java.nio.charset.StandardCharsets.ISO_8859_1); + List events = new ArrayList<>(); + try (RecordingFile rf = new RecordingFile(file)) { + while (rf.hasMoreEvents()) events.add(rf.readEvent().toString()); + } + boolean leaked = false; + for (int i = 1; i < a.length; i++) { + String s = a[i]; + boolean inRaw = rawLatin.contains(s); + long inEvents = events.stream().filter(e -> e.contains(s)).count(); + leaked |= inRaw || inEvents > 0; + System.out.printf("%-26s RAW=%-5s EVENTS=%d%n", s, inRaw ? "LEAK" : "clean", inEvents); + } + System.exit(leaked ? 2 : 0); + } +} diff --git a/redaction/src/Workload.java b/redaction/src/Workload.java new file mode 100644 index 0000000..3a4aec3 --- /dev/null +++ b/redaction/src/Workload.java @@ -0,0 +1,17 @@ +/** + * A deliberately boring program. It does a little work so the JVM has something + * to record, then exits; the recording is written by -XX:StartFlightRecording. + * All the interesting part is on the command line, in -D flags and in the + * environment, which is where the secrets live. + * + * Run with: java Workload.java (single-file source launcher) + * Post: https://ankurm.com/jfr-data-redaction-jep-536-safe-flight-recordings-in-production/ + */ +public class Workload { + public static void main(String[] args) throws Exception { + long sum = 0; + for (int i = 0; i < 5_000_000; i++) sum += Integer.hashCode(i); + Thread.sleep(Long.getLong("workload.sleep.ms", 300)); + System.out.println("workload done, args=" + args.length + ", checksum=" + sum); + } +}