From: Thomas Lamprecht <t.lamprecht@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH common 03/10] schema: add helpers to redact sensitive values
Date: Sun, 1 Nov 2026 22:34:52 +0100 [thread overview]
Message-ID: <20261101213835.353604-4-t.lamprecht@proxmox.com> (raw)
In-Reply-To: <20261101213835.353604-1-t.lamprecht@proxmox.com>
The system report includes config files verbatim. Redacting them by
parsing and writing them out again would drop or normalize invalid
lines, which are often what support needs to see. So redaction works on
the raw text of key/value config files and only replaces the values of
properties marked as sensitive, in every section of the file. For
property strings only the sensitive keys are replaced, the rest of the
value stays readable. Empty values are kept, as they reveal nothing and
help to spot configuration errors.
The value helper also takes parsed values, including array references
and unset values, so it can be reused for masking API output.
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
src/PVE/JSONSchema.pm | 43 ++++++++++++
test/json-schema-test.pl | 148 +++++++++++++++++++++++++++++++++++++++
2 files changed, 191 insertions(+)
diff --git a/src/PVE/JSONSchema.pm b/src/PVE/JSONSchema.pm
index e4225df..12ef6c2 100644
--- a/src/PVE/JSONSchema.pm
+++ b/src/PVE/JSONSchema.pm
@@ -2656,6 +2656,49 @@ sub dump_config {
return $data;
}
+sub redact_sensitive_value($schema, $value, $placeholder) {
+ return $value if !defined($value);
+ return [map { redact_sensitive_value($schema, $_, $placeholder) } $value->@*]
+ if ref($value) eq 'ARRAY';
+ return $value if $value !~ m/\S/;
+ return $placeholder if $schema->{sensitive};
+ return redact_sensitive_value($schema->{items}, $value, $placeholder) if $schema->{items};
+
+ my $format = $schema->{format};
+ $format = get_format($format) if defined($format) && !ref($format);
+ return $value if ref($format) ne 'HASH';
+
+ my ($default_key) = grep { $format->{$_}->{default_key} } keys $format->%*;
+ my $key_schema = sub($key) {
+ my $key_schema = $format->{$key} // return;
+ return defined($key_schema->{alias}) ? $format->{ $key_schema->{alias} } : $key_schema;
+ };
+
+ my @parts = split(/,/, $value, -1);
+ for my $part (@parts) {
+ if (my ($key, $key_value) = $part =~ m/^([^=]+)=(.*)$/) {
+ my $schema = $key_schema->($key) or next;
+ $part = "$key=" . redact_sensitive_value($schema, $key_value, $placeholder);
+ } elsif (defined($default_key)) {
+ $part = redact_sensitive_value($format->{$default_key}, $part, $placeholder);
+ }
+ }
+ return join(',', @parts);
+}
+
+sub redact_raw_config($schema, $raw, $placeholder) {
+ my $props = $schema->{properties} or die "got strange schema\n";
+
+ my @lines = split(/\n/, $raw, -1);
+ for my $line (@lines) {
+ my ($prefix, $key, $value) = $line =~ m/^(\s*(\S+?):\s*)(.*)$/ or next;
+ my $prop = $props->{$key} or next;
+ $prop = $props->{ $prop->{alias} } // next if defined($prop->{alias});
+ $line = $prefix . redact_sensitive_value($prop, $value, $placeholder);
+ }
+ return join("\n", @lines);
+}
+
# helpers used to generate our manual pages
my $find_schema_default_key = sub {
diff --git a/test/json-schema-test.pl b/test/json-schema-test.pl
index f1c968f..28c8efd 100755
--- a/test/json-schema-test.pl
+++ b/test/json-schema-test.pl
@@ -863,4 +863,152 @@ for my $test ($check_one_of->@*) {
};
}
+PVE::JSONSchema::register_format(
+ 'test-redact',
+ {
+ name => { type => 'string', default_key => 1, optional => 1 },
+ secret => { type => 'string', sensitive => 1, optional => 1 },
+ 'old-secret' => { alias => 'secret' },
+ },
+);
+
+my $redact_value_tests = [
+ {
+ name => 'sensitive value',
+ schema => { type => 'string', sensitive => 1 },
+ in => 'hunter2',
+ out => '***',
+ },
+ {
+ name => 'other value',
+ schema => { type => 'string' },
+ in => 'visible',
+ out => 'visible',
+ },
+ {
+ name => 'sensitive sub-key',
+ schema => { type => 'string', format => 'test-redact' },
+ in => 'name=foo,secret=bar',
+ out => 'name=foo,secret=***',
+ },
+ {
+ name => 'sensitive sub-key via alias',
+ schema => { type => 'string', format => 'test-redact' },
+ in => 'foo,old-secret=bar',
+ out => 'foo,old-secret=***',
+ },
+ {
+ name => 'sensitive default key',
+ schema => {
+ type => 'string',
+ format => { secret => { type => 'string', default_key => 1, sensitive => 1 } },
+ },
+ in => 'bar,,',
+ out => '***,,',
+ },
+ {
+ name => 'format without sensitive sub-keys',
+ schema => { type => 'string', format => { name => { type => 'string' } } },
+ in => 'not=a,valid',
+ out => 'not=a,valid',
+ },
+ {
+ name => 'empty sensitive value',
+ schema => { type => 'string', sensitive => 1 },
+ in => '',
+ out => '',
+ },
+ {
+ name => 'nested format',
+ schema => {
+ type => 'string',
+ format => {
+ inner => { type => 'string', format => 'test-redact' },
+ other => { type => 'string' },
+ },
+ },
+ in => 'other=x,inner=secret=bar',
+ out => 'other=x,inner=secret=***',
+ },
+ {
+ name => 'unset value',
+ schema => { type => 'string', sensitive => 1 },
+ in => undef,
+ out => undef,
+ },
+ {
+ name => 'array item',
+ schema => { type => 'array', items => { type => 'string', format => 'test-redact' } },
+ in => 'foo,secret=bar',
+ out => 'foo,secret=***',
+ },
+ {
+ name => 'array of items',
+ schema => { type => 'array', items => { type => 'string', format => 'test-redact' } },
+ in => ['foo,secret=bar', 'baz'],
+ out => ['foo,secret=***', 'baz'],
+ },
+ {
+ name => 'sensitive array',
+ schema => { type => 'array', sensitive => 1, items => { type => 'string' } },
+ in => ['foo', 'bar'],
+ out => ['***', '***'],
+ },
+];
+
+for my $test ($redact_value_tests->@*) {
+ my $res = PVE::JSONSchema::redact_sensitive_value($test->{schema}, $test->{in}, '***');
+ is_deeply($res, $test->{out}, "redact $test->{name}");
+}
+
+my $redact_config_schema = {
+ type => 'object',
+ properties => {
+ password => { type => 'string', sensitive => 1 },
+ 'old-password' => { type => 'string', alias => 'password' },
+ options => { type => 'string', format => 'test-redact' },
+ name => { type => 'string' },
+ tokens => { type => 'array', items => { type => 'string', sensitive => 1 } },
+ },
+};
+my $redact_config_in = <<'EOF';
+#password: in a comment
+password: hunter2
+old-password: aliased
+options: foo,secret=bar
+name: visible
+tokens: first
+tokens: second
+invalid line
+
+[snapshot]
+ password: older
+[special:cloudinit]
+password: applied
+password:
+EOF
+my $redact_config_out = <<'EOF';
+#password: in a comment
+password: ***
+old-password: ***
+options: foo,secret=***
+name: visible
+tokens: ***
+tokens: ***
+invalid line
+
+[snapshot]
+ password: ***
+[special:cloudinit]
+password: ***
+password:
+EOF
+is(
+ PVE::JSONSchema::redact_raw_config($redact_config_schema, $redact_config_in, '***'),
+ $redact_config_out,
+ 'redact key/value config',
+);
+eval { PVE::JSONSchema::redact_raw_config({ type => 'object' }, $redact_config_in, '***') };
+like($@, qr/strange schema/, 'redact key/value config fails without properties');
+
done_testing();
--
2.47.3
next prev parent reply other threads:[~2026-11-01 21:40 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-01 21:34 [PATCH common,qemu-server,network,manager 0/10] mark secrets in schemas and redact them from the system report Thomas Lamprecht
2026-11-01 21:34 ` [PATCH common 01/10] section config: do not add isolated property lists on lookups Thomas Lamprecht
2026-11-01 21:34 ` [PATCH common 02/10] schema: allow marking properties as sensitive Thomas Lamprecht
2026-11-01 21:34 ` Thomas Lamprecht [this message]
2026-11-01 21:34 ` [PATCH common 04/10] section config: add redaction of sensitive values in raw configs Thomas Lamprecht
2026-11-01 21:34 ` [PATCH qemu-server 05/10] cloud-init: mark the password as sensitive Thomas Lamprecht
2026-11-01 21:34 ` [PATCH qemu-server 06/10] config: hide sensitive values of unparsable lines in warnings Thomas Lamprecht
2026-11-01 21:34 ` [PATCH network 07/10] sdn: ipam, dns: mark API tokens as sensitive Thomas Lamprecht
2026-11-01 21:34 ` [PATCH manager 08/10] report: redact sensitive values from guest, storage and SDN configs Thomas Lamprecht
2026-11-01 21:34 ` [PATCH manager 09/10] report: redact credentials in URLs, options and environment variables Thomas Lamprecht
2026-11-01 21:34 ` [PATCH manager 10/10] pvereport: man page: document which values get redacted Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261101213835.353604-4-t.lamprecht@proxmox.com \
--to=t.lamprecht@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.