[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