From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 0F35C1FF09D for ; Sun, 01 Nov 2026 22:39:04 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 0631C21687; Sun, 01 Nov 2026 22:38:52 +0100 (CET) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH common 04/10] section config: add redaction of sensitive values in raw configs Date: Sun, 1 Nov 2026 22:34:53 +0100 Message-ID: <20261101213835.353604-5-t.lamprecht@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261101213835.353604-1-t.lamprecht@proxmox.com> References: <20261101213835.353604-1-t.lamprecht@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793569121028 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.414 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: RYN275UW4HHTNQVQBTMGN7FPOHN3NBKY X-Message-ID-Hash: RYN275UW4HHTNQVQBTMGN7FPOHN3NBKY X-MailFrom: t.lamprecht@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Section config files like the SDN IPAM and DNS configs hold API tokens and are included verbatim in the system report, so they need the same raw-text redaction as key/value config files. Sections are delimited as in parse_config, so each value is checked against the schema of the section the parser assigns it to. Sections of a type without a registered plugin are still checked against the properties that are not isolated to a plugin. Signed-off-by: Thomas Lamprecht --- src/PVE/SectionConfig.pm | 48 +++++++++++++++++++ .../section_config_property_isolation_test.pl | 43 +++++++++++++++++ test/section_config_test.pl | 40 ++++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/src/PVE/SectionConfig.pm b/src/PVE/SectionConfig.pm index 1febf76..04a79de 100644 --- a/src/PVE/SectionConfig.pm +++ b/src/PVE/SectionConfig.pm @@ -1807,6 +1807,54 @@ sub write_config { return $out; } +=head3 $base->redact_raw_config(...) + +=head3 $base->redact_raw_config($raw, $placeholder) + + $redacted = PVE::Example::BasePlugin->redact_raw_config($raw, $placeholder) + $redacted = $class->redact_raw_config($raw, $placeholder) + +Returns the contents of a C> file with the value of every +property marked as C replaced by C<$placeholder>, including sensitive +keys of property strings. + +Works on the raw text, so lines that +Cparse_config(...) >>> would skip as invalid are +kept. As in Cparse_config(...) >>>, a section ends +at the next empty line. Outside of sections of a known +Itype() >>>, only properties that are not isolated to a +plugin are recognized. + +=cut + +sub redact_raw_config($class, $raw, $placeholder) { + my ($type, $in_section) = ('', 0); + my @lines = split(/\n/, $raw, -1); + for my $line (@lines) { + next if $line =~ m/^\s*#/; + + if ($line eq '') { + ($type, $in_section) = ('', 0); + next; + } + + if (!$in_section) { + my ($header_type) = $class->parse_section_header($line); + if (defined($header_type)) { + ($type, $in_section) = ($header_type, 1); + next; + } + } + + if (my ($prefix, $key, $value) = $line =~ m/^(\s+(\S+)\s+)(.*)$/) { + my $schema = $class->get_property_schema($type, $key) or next; + $line = + $prefix . PVE::JSONSchema::redact_sensitive_value($schema, $value, $placeholder); + } + } + return join("\n", @lines); +} + sub assert_if_modified { my ($cfg, $digest) = @_; diff --git a/test/section_config_property_isolation_test.pl b/test/section_config_property_isolation_test.pl index 11b3142..a487952 100755 --- a/test/section_config_property_isolation_test.pl +++ b/test/section_config_property_isolation_test.pl @@ -85,6 +85,7 @@ sub properties { description => 'Another field', type => 'string', optional => 1, + sensitive => 1, }, arrayfield => { description => "Array Field with property string", @@ -395,6 +396,7 @@ my sub make_schema($is_update) { description => 'Another field', type => 'string', optional => 1, + sensitive => 1, }, arrayfield => { description => "Array Field with property string", @@ -481,6 +483,47 @@ ok( "property lookup does not add a property list", ); +my $redact_in = <<"EOF"; +one: a + another secret + +two: b + another visible + +unknown: c + another isolated + +two: d +one: d-ignored + another visible + +one: e +two: e-ignored + another secret + another +EOF +my $redact_out = <<"EOF"; +one: a + another + +two: b + another visible + +unknown: c + another isolated + +two: d +one: d-ignored + another visible + +one: e +two: e-ignored + another + another +EOF +is(Conf->redact_raw_config($redact_in, ''), $redact_out, + "property-isolation redact test"); + done_testing(); 1; diff --git a/test/section_config_test.pl b/test/section_config_test.pl index 2cdff98..3e52834 100755 --- a/test/section_config_test.pl +++ b/test/section_config_test.pl @@ -75,6 +75,7 @@ sub properties { another => { description => 'Another field', type => 'string', + sensitive => 1, }, }; } @@ -115,6 +116,7 @@ sub properties { subfield1 => { type => 'string', description => 'first subfield', + sensitive => 1, }, subfield2 => { type => 'integer', @@ -300,6 +302,7 @@ my $expected_create_schema = { 'subfield1' => { 'description' => 'first subfield', 'type' => 'string', + 'sensitive' => 1, }, }, 'type' => 'string', @@ -311,6 +314,7 @@ my $expected_create_schema = { 'description' => 'Another field', 'optional' => 1, 'type' => 'string', + 'sensitive' => 1, }, }, }; @@ -369,6 +373,7 @@ my $expected_update_schema = { subfield1 => { description => 'first subfield', type => 'string', + sensitive => 1, }, }, }, @@ -379,11 +384,46 @@ my $expected_update_schema = { description => 'Another field', optional => 1, type => 'string', + sensitive => 1, }, }, }; is_deeply($update_schema, $expected_update_schema, "update schema test"); +my $redact_in = <<"EOF"; +one: t1 + common foo + another secret1 + +# another: comment +two: t2 + field2 5 + another secret2 +invalid line + arrayfield subfield2=2,subfield1=secret3 + +bad: unknown + another secret4 + unknownfield visible +EOF +my $redact_out = <<"EOF"; +one: t1 + common foo + another + +# another: comment +two: t2 + field2 5 + another +invalid line + arrayfield subfield2=2,subfield1= + +bad: unknown + another + unknownfield visible +EOF +is(Conf->redact_raw_config($redact_in, ''), $redact_out, "redact raw config"); + done_testing(); 1; -- 2.47.3