Add Cloudmin virtualization host firewall profile

This commit is contained in:
Ilia Ross
2026-10-06 23:06:08 +02:00
parent 11be3726f7
commit afd6fad51e
4 changed files with 59 additions and 1 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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',
);

View File

@@ -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');