From afd6fad51e45bc3a61a347a8a0cbbd0a2f119f84 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Tue, 6 Oct 2026 23:06:08 +0200 Subject: [PATCH] Add Cloudmin virtualization host firewall profile --- CHANGELOG.md | 1 + nftables/lang/en | 2 ++ nftables/nftables-lib.pl | 23 ++++++++++++++++++++++- nftables/t/run-tests.t | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 351ca3544..958394743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Changelog #### 2.671 (October, 2026) +* Add a Cloudmin virtualization host profile to the nftables module * Fix two-factor authentication validation to use the Webmin configuration and data directories passed by the caller * Fix HTML escaping of module, script and session fields on the Webmin Actions Log details page * Fix mailbox attachment option injection diff --git a/nftables/lang/en b/nftables/lang/en index e41945759..af9d7548e 100644 --- a/nftables/lang/en +++ b/nftables/lang/en @@ -135,6 +135,8 @@ setup_profile_dns=DNS server setup_profile_dns_desc=Management access plus DNS, DNS-over-TLS, DHCPv6 client and mDNS setup_profile_virtualmin=Virtualmin hosting server setup_profile_virtualmin_desc=Management, web, mail, DNS, FTP, Usermin and passive FTP ports +setup_profile_cloudmin=Cloudmin virtualization host +setup_profile_cloudmin_desc=Management access and guest DNS/DHCP setup_profile_locked=Locked-down server setup_profile_locked_desc=Drop input, forward and output traffic with no service exceptions setup_profile_custom=Custom selected services diff --git a/nftables/nftables-lib.pl b/nftables/nftables-lib.pl index 8986bf2dc..1ed41230e 100644 --- a/nftables/nftables-lib.pl +++ b/nftables/nftables-lib.pl @@ -2990,6 +2990,14 @@ return ( ssh_alt webmin_range usermin passive_ftp) ] }, + { + 'id' => 'cloudmin', + 'name' => text('setup_profile_cloudmin'), + 'desc' => text('setup_profile_cloudmin_desc'), + 'input' => 'drop', + 'services' => [qw(ssh webmin)], + 'guest_bridges' => [ 'virbr*' ] + }, { 'id' => 'locked', 'name' => text('setup_profile_locked'), @@ -3852,12 +3860,16 @@ my $table = { } } }; +# Omitted policies leave those hooks to another manager, such as libvirt. +foreach my $chain (keys %{$table->{'chains'}}) { + delete $table->{'chains'}->{$chain} if (!defined($profile->{$chain})); + } return $table if ($profile_id eq 'allow_all'); add_profile_rule($table, 'input', 'ct state established,related accept'); add_profile_rule($table, 'input', 'iif "lo" accept'); add_profile_rule($table, 'input', 'meta l4proto { icmp, ipv6-icmp } accept'); -if ($profile->{'output'} eq 'drop') { +if (($profile->{'output'} || '') eq 'drop') { add_profile_rule($table, 'output', 'ct state established,related accept'); add_profile_rule($table, 'output', 'oif "lo" accept'); @@ -3884,6 +3896,14 @@ add_profile_port_set($table, $profile_id, \%ports); foreach my $rule (@special_rules) { add_profile_rule($table, 'input', $rule); } + +# Guest DNS and DHCP reach the host only through the profile's bridges. +foreach my $bridge (@{$profile->{'guest_bridges'} || [ ]}) { + add_profile_rule($table, 'input', + qq(iifname "$bridge" udp dport { 53, 67, 547 } accept)); + add_profile_rule($table, 'input', + qq(iifname "$bridge" tcp dport 53 accept)); + } return $table; } @@ -3999,6 +4019,7 @@ my %names = ( 'mail' => 'profile_mail', 'dns' => 'profile_dns', 'virtualmin' => 'profile_hosting', + 'cloudmin' => 'profile_virtualization', 'locked' => 'profile_locked', 'custom' => 'profile_custom', ); diff --git a/nftables/t/run-tests.t b/nftables/t/run-tests.t index 52a541310..9b14cce90 100755 --- a/nftables/t/run-tests.t +++ b/nftables/t/run-tests.t @@ -520,6 +520,40 @@ ok(scalar(grep { $_ eq '2022' } is(profile_base_table_name('virtualmin'), 'webmin_profile_hosting', 'Virtualmin profiles use a Webmin-prefixed table name'); +# The virtualization profile must work through both the setup UI and installer. +my %profiles = map { $_->{id} => $_ } setup_profiles(); +is($profiles{cloudmin}->{name}, 'Cloudmin virtualization host', + 'Cloudmin profile appears in the setup profile list'); +is(profile_base_table_name('cloudmin'), 'webmin_profile_virtualization', + 'Cloudmin follows the existing Webmin table naming convention'); +my $cloudmin_table = create_profile_ruleset( + profile_base_table_name('cloudmin'), 'cloudmin', '*'); +is_deeply([sort keys %{$cloudmin_table->{chains}}], ['input'], + 'Cloudmin leaves forwarding and output to other managers'); +is($cloudmin_table->{chains}->{input}->{policy}, 'drop', + 'Cloudmin denies unlisted host services'); +is_deeply($cloudmin_table->{sets}->{profile_virtualization_ports}->{elements}, + [ '2022', '2200', '2223', '10000' ], + 'Cloudmin allows configured SSH and Webmin ports only'); +my $cloudmin_rules = dump_nftables_save($cloudmin_table); +like($cloudmin_rules, qr/iifname "virbr\*" udp dport \{ 53, 67, 547 \} accept/, + 'Cloudmin allows guest DNS and DHCP only on libvirt bridges'); +like($cloudmin_rules, qr/iifname "virbr\*" tcp dport 53 accept/, + 'Cloudmin allows guest DNS over TCP'); +unlike($cloudmin_rules, qr/(?:hook forward|hook postrouting|masquerade|2049)/, + 'Cloudmin does not generate guest forwarding, NAT or public NFS access'); +my $cloudmin_custom = create_profile_ruleset('custom_vm_host', 'cloudmin', ['https']); +is_deeply($cloudmin_custom->{sets}->{profile_virtualization_tcp_ports}->{elements}, + [ '443' ], 'Cloudmin honors services selected in the setup UI'); +{ + no warnings 'redefine'; + local *get_nftables_save = sub { ($cloudmin_table) }; + is(profile_table_name('cloudmin'), 'webmin_profile_virtualization_1', + 'Cloudmin setup avoids colliding with an existing profile table'); +} +is($profile_table->{chains}->{forward}->{policy}, 'drop', + 'Virtualmin retains its existing forwarding policy'); + # The saved configuration is the system's own nftables file, so re-writing # it must not discard anything the module does not model my $sysfile = write_ruleset($confdir, 'system.nft', <<'EOF');