[pve-devel] [RFC PATCH container] 'feature' config option
Wolfgang Bumiller
w.bumiller at proxmox.com
Mon Mar 14 15:08:04 CET 2016
This introduces a 'features' option which currently contains
3 AppArmor related options: netmount, blockmount and
nesting. These change the container's AppArmor profile and
are thus incompatible with a custom 'lxc.aa_profile' option,
and are used to allow mounting of network or block devices
as well as nested containers. (None of them are recommended
as they come with several security concerns.)
There's now a new /lxc/$vmid/features API call which returns
a list of available features with their descriptions and
whether the current user is allowed to use them. (Currently
all 3 features require being logged in as root at pam to be
activated, but no special permissions to be _deactivated_.)
Next to querying this API call also allows modifying the
features, but this can also be done via the regular
/lxc/vmid/config API call.
At build-time the AppArmor profiles for all combinations of
features are produced and installed as
/etc/apparmor.d/lxc/lxc-pve-profiles.
Since each feature can have its own 'permissions' and are
thus compared to the old setting when updating the config
the check_ct_modify_config_perm() now gets the old config
passed as parameter for more flexibility.
---
debian/postinst | 2 +
src/PVE/API2/LXC.pm | 10 ++-
src/PVE/API2/LXC/Config.pm | 6 +-
src/PVE/API2/LXC/Features.pm | 194 +++++++++++++++++++++++++++++++++++++++++++
src/PVE/API2/LXC/Makefile | 6 +-
src/PVE/LXC.pm | 69 ++++++++++++++-
src/PVE/LXC/Config.pm | 42 ++++++++++
7 files changed, 320 insertions(+), 9 deletions(-)
create mode 100644 src/PVE/API2/LXC/Features.pm
diff --git a/debian/postinst b/debian/postinst
index 6323a0e..0f94d15 100755
--- a/debian/postinst
+++ b/debian/postinst
@@ -33,6 +33,8 @@ case "$1" in
/usr/sbin/pve-update-lxc-config
fi
+ /sbin/apparmor_parser -r -W -T /etc/apparmor.d/lxc-containers
+
# There are three sub-cases:
if test "${2+set}" != set; then
# We're being installed by an ancient dpkg which doesn't remember
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index 12e97f4..fdccddd 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -19,6 +19,7 @@ use PVE::LXC::Migrate;
use PVE::API2::LXC::Config;
use PVE::API2::LXC::Status;
use PVE::API2::LXC::Snapshot;
+use PVE::API2::LXC::Features;
use PVE::HA::Env::PVE2;
use PVE::HA::Config;
use PVE::JSONSchema qw(get_standard_option);
@@ -42,6 +43,11 @@ __PACKAGE__->register_method ({
});
__PACKAGE__->register_method ({
+ subclass => "PVE::API2::LXC::Features",
+ path => '{vmid}/features',
+});
+
+__PACKAGE__->register_method ({
subclass => "PVE::API2::Firewall::CT",
path => '{vmid}/firewall',
});
@@ -209,7 +215,7 @@ __PACKAGE__->register_method({
raise_perm_exc();
}
- PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, $pool, $param, []);
+ PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, $pool, {}, $param, []);
my $storage = extract_param($param, 'storage') // 'local';
@@ -1247,7 +1253,7 @@ __PACKAGE__->register_method({
die "no options specified\n" if !scalar(keys %$param);
- PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, $param, []);
+ PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, {}, $param, []);
my $storage_cfg = cfs_read_file("storage.cfg");
diff --git a/src/PVE/API2/LXC/Config.pm b/src/PVE/API2/LXC/Config.pm
index 0d0732e..f008a83 100644
--- a/src/PVE/API2/LXC/Config.pm
+++ b/src/PVE/API2/LXC/Config.pm
@@ -15,6 +15,7 @@ use PVE::RESTHandler;
use PVE::RPCEnvironment;
use PVE::LXC;
use PVE::LXC::Create;
+use PVE::API2::LXC::Features;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::RESTHandler);
@@ -112,7 +113,7 @@ __PACKAGE__->register_method({
my $delete_str = extract_param($param, 'delete');
my @delete = PVE::Tools::split_list($delete_str);
- PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, {}, [@delete]);
+ PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, {}, {}, [@delete]);
foreach my $opt (@delete) {
raise_param_exc({ delete => "you can't use '-$opt' and " .
@@ -124,14 +125,13 @@ __PACKAGE__->register_method({
}
}
- PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, $param, []);
-
my $storage_cfg = cfs_read_file("storage.cfg");
my $code = sub {
my $conf = PVE::LXC::Config->load_config($vmid);
PVE::LXC::Config->check_lock($conf);
+ PVE::LXC::check_ct_modify_config_perm($rpcenv, $authuser, $vmid, undef, $conf, $param, []);
PVE::Tools::assert_if_modified($digest, $conf->{digest});
diff --git a/src/PVE/API2/LXC/Features.pm b/src/PVE/API2/LXC/Features.pm
new file mode 100644
index 0000000..4d073fe
--- /dev/null
+++ b/src/PVE/API2/LXC/Features.pm
@@ -0,0 +1,194 @@
+package PVE::API2::LXC::Features;
+
+use strict;
+use warnings;
+
+use PVE::Tools qw(extract_param);
+use PVE::Exception qw(raise_param_exc);
+use PVE::Storage;
+use PVE::RESTHandler;
+use PVE::RPCEnvironment;
+use PVE::LXC;
+use PVE::JSONSchema qw(get_standard_option);
+use base qw(PVE::RESTHandler);
+
+my $apparmor_rules = {
+ netmount => [
+ 'mount fstype=nfs*,',
+ 'mount fstype=cifs,',
+ 'mount fstype=9p,',
+ ],
+ blockmount => [
+ # Equivalent of the lxc-container-default-with-mounting profile
+ 'mount fstype=ext*,',
+ 'mount fstype=xfs,',
+ 'mount fstype=btrfs,',
+ ],
+ nesting => [
+ # Equivalent of the lxc-container-default-with-nesting profile
+ '#include <abstractions/lxc/start-container>',
+ # NOTE: When we stop using cgmanager we need to add the following:
+ # 'mount fstype=cgroup -> /sys/fs/cgroup/**,',
+ 'deny /dev/.lxc/proc/** rw,',
+ 'deny /dev/.lxc/sys/** rw,',
+ 'mount fstype=proc -> /var/cache/lxc/**,',
+ 'mount fstype=sysfs -> /var/cache/lxc/**,',
+ 'mount options=(rw,bind),',
+ ],
+};
+
+sub map_combinations {
+ my ($values, $func, $current) = @_;
+ return if !@$values;
+ foreach my $i (0..@$values-1) {
+ &$func(@$current, $values->[$i]);
+ map_combinations([@$values[$i+1..@$values-1]],
+ $func,
+ [@$current, $values->[$i]]);
+ }
+};
+
+sub generate_apparmor_profiles {
+ my ($fh) = @_;
+ my $keys = [sort keys %$apparmor_rules];
+ print {$fh} "# lxc profile combinations\n";
+ map_combinations($keys, sub {
+ my (@options) = @_;
+ my $name = join('-', @options);
+ print {$fh} "\nprofile lxc-pve-$name";
+ print {$fh} " flags=(attach_disconnected,mediate_deleted)";
+ print {$fh} " {\n";
+ print {$fh} " #include <abstractions/lxc/container-base>\n";
+ foreach my $opt (@options) {
+ my $rules = $apparmor_rules->{$opt};
+ print {$fh} " ", join("\n ", @$rules), "\n";
+ }
+ print {$fh} "}\n";
+ });
+}
+
+__PACKAGE__->register_method({
+ name => 'features',
+ path => '',
+ method => 'GET',
+ proxyto => 'node',
+ description => "Get available feature information.",
+ permissions => {
+ check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
+ },
+ },
+ returns => {
+ type => 'object',
+ properties => {
+ features => { type => 'string', optional => 1 },
+ available => {
+ type => 'array',
+ items => {
+ type => 'object',
+ properties => {
+ id => { type => 'string' },
+ name => { type => 'string' },
+ description => { type => 'string' },
+ allowed => { type => 'boolean' },
+ }
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+
+ my $authuser = $rpcenv->get_user();
+
+ my $conf = PVE::LXC::Config->load_config($param->{vmid});
+
+ my $all_options = $PVE::LXC::Config::ct_features;
+ my $available = [];
+ foreach my $id (keys %$all_options) {
+ my $allowed = PVE::LXC::check_feature_perms($rpcenv, $authuser, $id, $param, 1);
+ my $feature = $all_options->{$id};
+ push @$available, { id => $id,
+ name => $feature->{name},
+ description => $feature->{description},
+ allowed => $allowed ? 1 : 0,
+ };
+ }
+
+ my $res = { available => $available };
+ if (my $features = $conf->{features}) {
+ $res->{features} = $features;
+ }
+ return $res;
+ }});
+
+__PACKAGE__->register_method({
+ name => 'update_features',
+ path => '',
+ method => 'PUT',
+ protected => 1,
+ proxyto => 'node',
+ description => "Set VM features.",
+ permissions => {
+ check => ['perm', '/vms/{vmid}', [ 'VM.Config.Options' ]],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
+ features => {
+ type => 'string',
+ format => $PVE::LXC::Config::feature_desc,
+ },
+ digest => {
+ type => 'string',
+ description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
+ maxLength => 40,
+ optional => 1,
+ }
+ },
+ },
+ returns => { type => 'null', },
+ code => sub {
+ my ($param) = @_;
+
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $authuser = $rpcenv->get_user();
+
+ my $vmid = $param->{vmid};
+ my $features = extract_param($param, 'features');
+
+ my $digest = extract_param($param, 'digest');
+
+ my $code = sub {
+ my $conf = PVE::LXC::Config->load_config($vmid);
+ PVE::LXC::Config->check_lock($conf);
+
+ PVE::Tools::assert_if_modified($digest, $conf->{digest});
+
+ if (!length($features)) {
+ delete $conf->{features};
+ } else {
+ PVE::LXC::check_modify_features($rpcenv, $authuser, $vmid, undef, $conf->{features}, $features);
+ $conf->{features} = $features;
+ }
+ PVE::LXC::Config->write_config($vmid, $conf);
+
+ my $storage_cfg = PVE::Storage::config();
+ PVE::LXC::update_lxc_config($storage_cfg, $vmid, $conf);
+ };
+
+ PVE::LXC::Config->lock_config($vmid, $code);
+
+ return undef;
+ }});
+
+1;
diff --git a/src/PVE/API2/LXC/Makefile b/src/PVE/API2/LXC/Makefile
index f372d95..dd1b214 100644
--- a/src/PVE/API2/LXC/Makefile
+++ b/src/PVE/API2/LXC/Makefile
@@ -1,8 +1,12 @@
-SOURCES=Config.pm Status.pm Snapshot.pm
+SOURCES=Config.pm Status.pm Snapshot.pm Features.pm
+
+LXCPROFILEDIR=${DESTDIR}/etc/apparmor.d/lxc
.PHONY: install
install:
install -d -m 0755 ${PERLDIR}/PVE/API2/LXC
for i in ${SOURCES}; do install -D -m 0644 $$i ${PERLDIR}/PVE/API2/LXC/$$i; done
+ install -dm755 ${LXCPROFILEDIR}
+ perl -e 'require "./Features.pm"; PVE::API2::LXC::Features::generate_apparmor_profiles(\*STDOUT);' > ${LXCPROFILEDIR}/lxc-pve-profiles
diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 58be169..0faa972 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -12,7 +12,7 @@ use File::Spec;
use Cwd qw();
use Fcntl qw(O_RDONLY);
-use PVE::Exception qw(raise_perm_exc);
+use PVE::Exception qw(raise_perm_exc raise_param_exc);
use PVE::Storage;
use PVE::SafeSyslog;
use PVE::INotify;
@@ -420,11 +420,27 @@ sub update_lxc_config {
$raw .= "lxc.network.mtu = $d->{mtu}\n" if defined($d->{mtu});
}
+ my $features = PVE::LXC::Config->parse_features($conf->{features});
+
+ my @aa_features = grep {
+ $features->{$_} && exists($PVE::LXC::Config::apparmor_features->{$_})
+ } keys %$features;
+ if (@aa_features) {
+ my $aa_profile = join('-', 'lxc-pve', sort(@aa_features));
+ $raw .= "lxc.aa_profile = $aa_profile\n";
+ }
+
+
if (my $lxcconf = $conf->{lxc}) {
foreach my $entry (@$lxcconf) {
my ($k, $v) = @$entry;
$netcount++ if $k eq 'lxc.network.type';
- $raw .= "$k = $v\n";
+ if (@aa_features && $k eq 'lxc.aa_profile') {
+ my $options = join(', ', @aa_features);
+ die "$k overrides AppArmor related feature options: $options\n";
+ } else {
+ $raw .= "$k = $v\n";
+ }
}
}
@@ -847,8 +863,48 @@ sub template_create {
PVE::LXC::Config->write_config($vmid, $conf);
}
+sub check_feature_perms {
+ my ($rpcenv, $authuser, $feature_name, $uri_param, $noerr) = @_;
+ my $feature = $PVE::LXC::Config::ct_features->{$feature_name};
+ if (!$feature) {
+ raise_param_exc({option => "unknown option: '$feature_name'"}) if !$noerr;
+ return 0;
+ }
+
+ my $perms = $feature->{permissions};
+ # If no permissions are defined only root can change a feature
+ if (!$perms && $authuser ne 'root at pam') {
+ raise_perm_exc("only root\@pam can enable feature '$feature_name'") if !$noerr;
+ return 0;
+ }
+ eval { $rpcenv->check_api2_permissions($perms, $authuser, $uri_param); };
+ raise_perm_exc("you do not have permission to enable feature '$feature_name'") if $@ && !$noerr;
+ return !$@;
+}
+
+sub check_modify_features {
+ my ($rpcenv, $authuser, $vmid, $pool, $old_data, $new_data) = @_;
+
+ my $old = eval { PVE::LXC::Config->parse_features($old_data) } || {};
+ my $new = eval { PVE::LXC::Config->parse_features($new_data) };
+ raise_param_exc({option => "$@"}) if $@;
+
+ # Go through features which have been added or changed
+ foreach my $opt (keys %$new) {
+ my $oldvalue = $old->{$opt};
+ my $newvalue = $new->{$opt};
+ if (defined($oldvalue) != defined($newvalue) || $newvalue ne $oldvalue) {
+ # can always disable boolean features
+ my $desc = $PVE::LXC::Config::feature_desc->{$opt};
+ next if $desc->{type} eq 'boolean' && !$newvalue;
+ # check the permissions:
+ check_feature_perms($rpcenv, $authuser, $opt, { vmid => $vmid, pool => $pool }, 0);
+ }
+ }
+}
+
sub check_ct_modify_config_perm {
- my ($rpcenv, $authuser, $vmid, $pool, $newconf, $delete) = @_;
+ my ($rpcenv, $authuser, $vmid, $pool, $oldconf, $newconf, $delete) = @_;
return 1 if $authuser eq 'root at pam';
@@ -867,6 +923,13 @@ sub check_ct_modify_config_perm {
} elsif ($opt =~ m/^net\d+$/ || $opt eq 'nameserver' ||
$opt eq 'searchdomain' || $opt eq 'hostname') {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Network']);
+ } elsif ($opt eq 'features') {
+ if ($delete) {
+ $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
+ } else {
+ check_modify_features($rpcenv, $authuser, $vmid, $pool,
+ $oldconf->{features}, $newconf->{features});
+ }
} else {
$rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
}
diff --git a/src/PVE/LXC/Config.pm b/src/PVE/LXC/Config.pm
index 7c451ba..27bfa79 100644
--- a/src/PVE/LXC/Config.pm
+++ b/src/PVE/LXC/Config.pm
@@ -220,6 +220,7 @@ PVE::JSONSchema::register_standard_option('pve-lxc-snapshot-name', {
maxLength => 40,
});
+my $feature_desc = {};
my $confdesc = {
lock => {
optional => 1,
@@ -350,6 +351,11 @@ my $confdesc = {
description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
default => 0,
},
+ features => {
+ optional => 1,
+ type => 'string', format => $feature_desc,
+ description => "Enable various features.",
+ },
};
my $valid_lxc_conf_keys = {
@@ -561,6 +567,36 @@ for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
$confdesc->{"unused$i"} = $unuseddesc;
}
+our $apparmor_features = {
+ netmount => {
+ name => "Network mounts",
+ description => "Allow mounting network filesystems (nfs, cifs, 9p)",
+ #permissions => { check => [ 'perm', '/vms/{vmid}', [ 'VM.Config.Network' ]], },
+ },
+ blockmount => {
+ name => "Block mounts",
+ description => "Allow mounting block devices with ext2/3/4, xfs or btrfs filesystems.",
+ },
+ nesting => {
+ name => "Nesting",
+ description => "Allow nested containers.",
+ },
+};
+
+our $ct_features = {};
+
+foreach my $feature (keys %$apparmor_features) {
+ my $data = $apparmor_features->{$feature};
+ $ct_features->{$feature} = $data;
+ $feature_desc->{$feature} = {
+ optional => 1,
+ type => 'boolean',
+ description => $data->{description},
+ format_description => '[0|1]',
+ default => 0,
+ };
+}
+
sub parse_pct_config {
my ($filename, $raw) = @_;
@@ -1023,6 +1059,12 @@ sub parse_lxc_network {
return $res;
}
+sub parse_features {
+ my ($class, $data) = @_;
+ return {} if !$data;
+ return PVE::JSONSchema::parse_property_string($feature_desc, $data);
+}
+
sub option_exists {
my ($class, $name) = @_;
--
2.1.4
More information about the pve-devel
mailing list