[lxc-devel] [lxd/master] Initial apparmor profile for qemu

stgraber on Github lxc-bot at linuxcontainers.org
Tue Sep 15 22:54:46 UTC 2020


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 301 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20200915/bd2f042d/attachment.bin>
-------------- next part --------------
From ec0e232d3fef873b7441df2c75249e73bec397f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 15 Sep 2020 15:41:25 -0400
Subject: [PATCH 1/7] shared/subprocess: Set err on non-zero
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 shared/subprocess/proc.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/shared/subprocess/proc.go b/shared/subprocess/proc.go
index f78b1a1f70..27ee69d7e8 100644
--- a/shared/subprocess/proc.go
+++ b/shared/subprocess/proc.go
@@ -177,6 +177,9 @@ func (p *Process) start(fds []*os.File) error {
 
 		exitcode := int64(procstate.ExitCode())
 		p.exitCode = exitcode
+		if p.exitCode != 0 {
+			p.exitErr = fmt.Errorf("Process exited with a non-zero value")
+		}
 		close(p.chExit)
 	}()
 

From 2bc7219b6078710382c8a8526bc7828737729a49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 15 Sep 2020 15:25:13 -0400
Subject: [PATCH 2/7] lxd/instances/qemu: Use subprocess
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/instance/drivers/driver_qemu.go | 29 +++++++++++++++++++++--------
 1 file changed, 21 insertions(+), 8 deletions(-)

diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go
index c3b3d80d35..bab4b84d61 100644
--- a/lxd/instance/drivers/driver_qemu.go
+++ b/lxd/instance/drivers/driver_qemu.go
@@ -57,6 +57,7 @@ import (
 	"github.com/lxc/lxd/shared/logger"
 	"github.com/lxc/lxd/shared/logging"
 	"github.com/lxc/lxd/shared/osarch"
+	"github.com/lxc/lxd/shared/subprocess"
 	"github.com/lxc/lxd/shared/termios"
 	"github.com/lxc/lxd/shared/units"
 )
@@ -827,13 +828,14 @@ func (vm *qemu) Start(stateful bool) error {
 		forkLimitsCmd = append(forkLimitsCmd, fmt.Sprintf("fd=%d", 3+i))
 	}
 
-	cmd := exec.Command(vm.state.OS.ExecPath, append(forkLimitsCmd, qemuCmd...)...)
-	var stdout bytes.Buffer
-	var stderr bytes.Buffer
-	cmd.Stdout = &stdout
-	cmd.Stderr = &stderr
+	// Setup background process.
+	p, err := subprocess.NewProcess(vm.state.OS.ExecPath, append(forkLimitsCmd, qemuCmd...), vm.EarlyLogFilePath(), vm.EarlyLogFilePath())
+	if err != nil {
+		return err
+	}
 
 	// Open any extra files and pass their file handles to qemu command.
+	files := []*os.File{}
 	for _, file := range fdFiles {
 		info, err := os.Stat(file)
 		if err != nil {
@@ -870,12 +872,18 @@ func (vm *qemu) Start(stateful bool) error {
 			defer f.Close() // Close file after qemu has started.
 		}
 
-		cmd.ExtraFiles = append(cmd.ExtraFiles, f)
+		files = append(files, f)
 	}
 
-	err = cmd.Run()
+	err = p.StartWithFiles(files)
 	if err != nil {
-		err = errors.Wrapf(err, "Failed to run: %s: %s", strings.Join(cmd.Args, " "), strings.TrimSpace(string(stderr.Bytes())))
+		return err
+	}
+
+	_, err = p.Wait()
+	if err != nil {
+		stderr, _ := ioutil.ReadFile(vm.EarlyLogFilePath())
+		err = errors.Wrapf(err, "Failed to run: %s: %s", strings.Join(p.Args, " "), string(stderr))
 		op.Done(err)
 		return err
 	}
@@ -4389,6 +4397,11 @@ func (vm *qemu) LogPath() string {
 	return shared.LogPath(name)
 }
 
+// EarlyLogFilePath returns the instance's early log path.
+func (vm *qemu) EarlyLogFilePath() string {
+	return filepath.Join(vm.LogPath(), "qemu.early.log")
+}
+
 // LogFilePath returns the instance's log path.
 func (vm *qemu) LogFilePath() string {
 	return filepath.Join(vm.LogPath(), "qemu.log")

From 85ddc644cf42b184aeac11d1b47cce11c503cb1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 15 Sep 2020 17:44:48 -0400
Subject: [PATCH 3/7] lxd/instance: Add DevPaths
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/instance/drivers/driver_common.go | 9 +++++++++
 lxd/instance/drivers/driver_qemu.go   | 7 +++++++
 lxd/instance/instance_interface.go    | 3 +++
 3 files changed, 19 insertions(+)

diff --git a/lxd/instance/drivers/driver_common.go b/lxd/instance/drivers/driver_common.go
index 27d0fd1c81..7f3e9da6dd 100644
--- a/lxd/instance/drivers/driver_common.go
+++ b/lxd/instance/drivers/driver_common.go
@@ -11,6 +11,7 @@ import (
 // common provides structure common to all instance types.
 type common struct {
 	dbType          instancetype.Type
+	devPaths        []string
 	expandedConfig  map[string]string
 	expandedDevices deviceConfig.Devices
 	localConfig     map[string]string
@@ -77,3 +78,11 @@ func (c *common) expandDevices(profiles []api.Profile) error {
 
 	return nil
 }
+
+// DevPaths() returns a list of /dev devices which the instance requires access to.
+// This is function is only safe to call from within the security
+// packages as called during instance startup, the rest of the time this
+// will likely return nil.
+func (c *common) DevPaths() []string {
+	return c.devPaths
+}
diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go
index bab4b84d61..6a5f7cd2f7 100644
--- a/lxd/instance/drivers/driver_qemu.go
+++ b/lxd/instance/drivers/driver_qemu.go
@@ -640,6 +640,9 @@ func (vm *qemu) Start(stateful bool) error {
 	revert := revert.New()
 	defer revert.Fail()
 
+	// Start accumulating device paths.
+	vm.devPaths = []string{}
+
 	// Mount the instance's config volume.
 	_, err = vm.mount()
 	if err != nil {
@@ -2059,6 +2062,10 @@ func (vm *qemu) addDriveConfig(sb *strings.Builder, bootIndexes map[string]int,
 		}
 	}
 
+	if !strings.HasPrefix(driveConf.DevPath, "rbd:") {
+		vm.devPaths = append(vm.devPaths, driveConf.DevPath)
+	}
+
 	return qemuDrive.Execute(sb, map[string]interface{}{
 		"devName":   driveConf.DevName,
 		"devPath":   driveConf.DevPath,
diff --git a/lxd/instance/instance_interface.go b/lxd/instance/instance_interface.go
index f0c117f6e7..bfe9054a70 100644
--- a/lxd/instance/instance_interface.go
+++ b/lxd/instance/instance_interface.go
@@ -69,6 +69,9 @@ type Instance interface {
 	Delete() error
 	Export(w io.Writer, properties map[string]string) (api.ImageMetadata, error)
 
+	// Used for security.
+	DevPaths() []string
+
 	// Live configuration.
 	CGroupSet(key string, value string) error
 	VolatileSet(changes map[string]string) error

From 20581ad6585878588f3d9827d90a21be6610e0cd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 15 Sep 2020 17:42:02 -0400
Subject: [PATCH 4/7] lxd/apparmor: Fix unload/delete
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/apparmor/apparmor.go           | 14 +++++++-------
 lxd/apparmor/instance.go           |  4 ++--
 lxd/apparmor/instance_forkproxy.go |  4 ++--
 lxd/apparmor/network.go            |  8 ++++----
 4 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/lxd/apparmor/apparmor.go b/lxd/apparmor/apparmor.go
index ffb9491f56..2231964633 100644
--- a/lxd/apparmor/apparmor.go
+++ b/lxd/apparmor/apparmor.go
@@ -83,9 +83,9 @@ func deleteNamespace(state *state.State, name string) error {
 
 // hasProfile checks if the profile is already loaded.
 func hasProfile(state *state.State, name string) (bool, error) {
-	mangled := strings.Replace(name, "/", ".", -1)
+	mangled := strings.Replace(strings.Replace(strings.Replace(name, "/", ".", -1), "<", "", -1), ">", "", -1)
 
-	profilesPath := "/sys/kernel/security/apaprmor/policy/profiles"
+	profilesPath := "/sys/kernel/security/apparmor/policy/profiles"
 	if shared.PathExists(profilesPath) {
 		entries, err := ioutil.ReadDir(profilesPath)
 		if err != nil {
@@ -94,7 +94,7 @@ func hasProfile(state *state.State, name string) (bool, error) {
 
 		for _, entry := range entries {
 			fields := strings.Split(entry.Name(), ".")
-			if mangled == strings.Join(fields[0:len(fields)-2], ".") {
+			if mangled == strings.Join(fields[0:len(fields)-1], ".") {
 				return true, nil
 			}
 		}
@@ -122,12 +122,12 @@ func loadProfile(state *state.State, name string) error {
 }
 
 // unloadProfile removes the profile from the kernel.
-func unloadProfile(state *state.State, name string) error {
+func unloadProfile(state *state.State, fullName string, name string) error {
 	if !state.OS.AppArmorAvailable {
 		return nil
 	}
 
-	ok, err := hasProfile(state, name)
+	ok, err := hasProfile(state, fullName)
 	if err != nil {
 		return err
 	}
@@ -140,7 +140,7 @@ func unloadProfile(state *state.State, name string) error {
 }
 
 // deleteProfile unloads and delete profile and cache for a profile.
-func deleteProfile(state *state.State, name string) error {
+func deleteProfile(state *state.State, fullName string, name string) error {
 	if !state.OS.AppArmorAdmin {
 		return nil
 	}
@@ -150,7 +150,7 @@ func deleteProfile(state *state.State, name string) error {
 		return err
 	}
 
-	err = unloadProfile(state, name)
+	err = unloadProfile(state, fullName, name)
 	if err != nil {
 		return err
 	}
diff --git a/lxd/apparmor/instance.go b/lxd/apparmor/instance.go
index 1ae6f64b80..bccd46bf57 100644
--- a/lxd/apparmor/instance.go
+++ b/lxd/apparmor/instance.go
@@ -93,7 +93,7 @@ func InstanceUnload(state *state.State, inst instance) error {
 		return err
 	}
 
-	err = unloadProfile(state, instanceProfileFilename(inst))
+	err := unloadProfile(state, InstanceProfileName(inst), instanceProfileFilename(inst))
 	if err != nil {
 		return err
 	}
@@ -108,7 +108,7 @@ func InstanceParse(state *state.State, inst instance) error {
 
 // InstanceDelete removes the policy from cache/disk.
 func InstanceDelete(state *state.State, inst instance) error {
-	return deleteProfile(state, instanceProfileFilename(inst))
+	return deleteProfile(state, InstanceProfileName(inst), instanceProfileFilename(inst))
 }
 
 // instanceProfile generates the AppArmor profile template from the given instance.
diff --git a/lxd/apparmor/instance_forkproxy.go b/lxd/apparmor/instance_forkproxy.go
index 3bdb79ed11..96215cd77e 100644
--- a/lxd/apparmor/instance_forkproxy.go
+++ b/lxd/apparmor/instance_forkproxy.go
@@ -185,10 +185,10 @@ func ForkproxyLoad(state *state.State, inst instance, dev device) error {
 // ForkproxyUnload ensures that the instances's policy namespace is unloaded to free kernel memory.
 // This does not delete the policy from disk or cache.
 func ForkproxyUnload(state *state.State, inst instance, dev device) error {
-	return unloadProfile(state, forkproxyProfileFilename(inst, dev))
+	return unloadProfile(state, ForkproxyProfileName(inst, dev), forkproxyProfileFilename(inst, dev))
 }
 
 // ForkproxyDelete removes the policy from cache/disk.
 func ForkproxyDelete(state *state.State, inst instance, dev device) error {
-	return deleteProfile(state, forkproxyProfileFilename(inst, dev))
+	return deleteProfile(state, ForkproxyProfileName(inst, dev), forkproxyProfileFilename(inst, dev))
 }
diff --git a/lxd/apparmor/network.go b/lxd/apparmor/network.go
index f3efe516e5..e5b2698c79 100644
--- a/lxd/apparmor/network.go
+++ b/lxd/apparmor/network.go
@@ -85,14 +85,14 @@ func NetworkLoad(state *state.State, n network) error {
 // This does not delete the policy from disk or cache.
 func NetworkUnload(state *state.State, n network) error {
 	// dnsmasq
-	err := unloadProfile(state, dnsmasqProfileFilename(n))
+	err := unloadProfile(state, DnsmasqProfileName(n), dnsmasqProfileFilename(n))
 	if err != nil {
 		return err
 	}
 
 	// forkdns
 	if n.Config()["bridge.mode"] == "fan" {
-		err := unloadProfile(state, forkdnsProfileFilename(n))
+		err := unloadProfile(state, ForkdnsProfileName(n), forkdnsProfileFilename(n))
 		if err != nil {
 			return err
 		}
@@ -103,13 +103,13 @@ func NetworkUnload(state *state.State, n network) error {
 
 // NetworkDelete removes the profiles from cache/disk.
 func NetworkDelete(state *state.State, n network) error {
-	err := deleteProfile(state, dnsmasqProfileFilename(n))
+	err := deleteProfile(state, DnsmasqProfileName(n), dnsmasqProfileFilename(n))
 	if err != nil {
 		return err
 	}
 
 	if n.Config()["bridge.mode"] == "fan" {
-		err := deleteProfile(state, forkdnsProfileFilename(n))
+		err := deleteProfile(state, ForkdnsProfileName(n), forkdnsProfileFilename(n))
 		if err != nil {
 			return err
 		}

From 2ece5ff996cf549401dd2871fb2fcb0a5e5f3671 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 15 Sep 2020 17:45:34 -0400
Subject: [PATCH 5/7] lxd/apparmor/instance: Sort context
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/apparmor/instance.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lxd/apparmor/instance.go b/lxd/apparmor/instance.go
index bccd46bf57..d6459e7262 100644
--- a/lxd/apparmor/instance.go
+++ b/lxd/apparmor/instance.go
@@ -131,15 +131,15 @@ func instanceProfile(state *state.State, inst instance) (string, error) {
 	// Render the profile.
 	var sb *strings.Builder = &strings.Builder{}
 	err = lxcProfileTpl.Execute(sb, map[string]interface{}{
-		"feature_unix":     unixSupported,
 		"feature_cgns":     state.OS.CGInfo.Namespacing,
 		"feature_cgroup2":  state.OS.CGInfo.Layout == cgroup.CgroupsUnified || state.OS.CGInfo.Layout == cgroup.CgroupsHybrid,
 		"feature_stacking": state.OS.AppArmorStacking && !state.OS.AppArmorStacked,
+		"feature_unix":     unixSupported,
+		"name":             InstanceProfileName(inst),
 		"namespace":        InstanceNamespaceName(inst),
 		"nesting":          shared.IsTrue(inst.ExpandedConfig()["security.nesting"]),
-		"name":             InstanceProfileName(inst),
-		"unprivileged":     !shared.IsTrue(inst.ExpandedConfig()["security.privileged"]) || state.OS.RunningInUserNS,
 		"raw":              rawContent,
+		"unprivileged":     !shared.IsTrue(inst.ExpandedConfig()["security.privileged"]) || state.OS.RunningInUserNS,
 	})
 	if err != nil {
 		return "", err

From fd082cf26bf1c220a1a146576fc421b8d54a2783 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 15 Sep 2020 14:04:15 -0400
Subject: [PATCH 6/7] lxd/apparmor: Prepare for qemu
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/apparmor/instance.go | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/lxd/apparmor/instance.go b/lxd/apparmor/instance.go
index d6459e7262..ab9880a26f 100644
--- a/lxd/apparmor/instance.go
+++ b/lxd/apparmor/instance.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 
 	"github.com/lxc/lxd/lxd/cgroup"
+	"github.com/lxc/lxd/lxd/instance/instancetype"
 	"github.com/lxc/lxd/lxd/project"
 	"github.com/lxc/lxd/lxd/state"
 	"github.com/lxc/lxd/shared"
@@ -18,6 +19,7 @@ type instance interface {
 	Project() string
 	Name() string
 	ExpandedConfig() map[string]string
+	Type() instancetype.Type
 }
 
 // InstanceProfileName returns the instance's AppArmor profile name.
@@ -43,9 +45,11 @@ func instanceProfileFilename(inst instance) string {
 
 // InstanceLoad ensures that the instances's policy is loaded into the kernel so the it can boot.
 func InstanceLoad(state *state.State, inst instance) error {
-	err := createNamespace(state, InstanceNamespaceName(inst))
-	if err != nil {
-		return err
+	if inst.Type() == instancetype.Container {
+		err := createNamespace(state, InstanceNamespaceName(inst))
+		if err != nil {
+			return err
+		}
 	}
 
 	/* In order to avoid forcing a profile parse (potentially slow) on
@@ -88,9 +92,11 @@ func InstanceLoad(state *state.State, inst instance) error {
 // InstanceUnload ensures that the instances's policy namespace is unloaded to free kernel memory.
 // This does not delete the policy from disk or cache.
 func InstanceUnload(state *state.State, inst instance) error {
-	err := deleteNamespace(state, InstanceNamespaceName(inst))
-	if err != nil {
-		return err
+	if inst.Type() == instancetype.Container {
+		err := deleteNamespace(state, InstanceNamespaceName(inst))
+		if err != nil {
+			return err
+		}
 	}
 
 	err := unloadProfile(state, InstanceProfileName(inst), instanceProfileFilename(inst))

From de3e3680ff59dd1db0b016bf23616d5d13612586 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 15 Sep 2020 14:04:25 -0400
Subject: [PATCH 7/7] lxd/apparmor: Add qemu profile
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Stéphane Graber <stgraber at ubuntu.com>
---
 lxd/apparmor/instance.go            | 66 ++++++++++++++++++++-----
 lxd/apparmor/instance_qemu.go       | 74 +++++++++++++++++++++++++++++
 lxd/instance/drivers/driver_qemu.go | 26 ++++++++++
 3 files changed, 153 insertions(+), 13 deletions(-)
 create mode 100644 lxd/apparmor/instance_qemu.go

diff --git a/lxd/apparmor/instance.go b/lxd/apparmor/instance.go
index ab9880a26f..77d266befd 100644
--- a/lxd/apparmor/instance.go
+++ b/lxd/apparmor/instance.go
@@ -11,6 +11,7 @@ import (
 	"github.com/lxc/lxd/lxd/instance/instancetype"
 	"github.com/lxc/lxd/lxd/project"
 	"github.com/lxc/lxd/lxd/state"
+	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
 )
 
@@ -20,6 +21,9 @@ type instance interface {
 	Name() string
 	ExpandedConfig() map[string]string
 	Type() instancetype.Type
+	LogPath() string
+	Path() string
+	DevPaths() []string
 }
 
 // InstanceProfileName returns the instance's AppArmor profile name.
@@ -136,19 +140,55 @@ func instanceProfile(state *state.State, inst instance) (string, error) {
 
 	// Render the profile.
 	var sb *strings.Builder = &strings.Builder{}
-	err = lxcProfileTpl.Execute(sb, map[string]interface{}{
-		"feature_cgns":     state.OS.CGInfo.Namespacing,
-		"feature_cgroup2":  state.OS.CGInfo.Layout == cgroup.CgroupsUnified || state.OS.CGInfo.Layout == cgroup.CgroupsHybrid,
-		"feature_stacking": state.OS.AppArmorStacking && !state.OS.AppArmorStacked,
-		"feature_unix":     unixSupported,
-		"name":             InstanceProfileName(inst),
-		"namespace":        InstanceNamespaceName(inst),
-		"nesting":          shared.IsTrue(inst.ExpandedConfig()["security.nesting"]),
-		"raw":              rawContent,
-		"unprivileged":     !shared.IsTrue(inst.ExpandedConfig()["security.privileged"]) || state.OS.RunningInUserNS,
-	})
-	if err != nil {
-		return "", err
+	if inst.Type() == instancetype.Container {
+		err = lxcProfileTpl.Execute(sb, map[string]interface{}{
+			"feature_cgns":     state.OS.CGInfo.Namespacing,
+			"feature_cgroup2":  state.OS.CGInfo.Layout == cgroup.CgroupsUnified || state.OS.CGInfo.Layout == cgroup.CgroupsHybrid,
+			"feature_stacking": state.OS.AppArmorStacking && !state.OS.AppArmorStacked,
+			"feature_unix":     unixSupported,
+			"name":             InstanceProfileName(inst),
+			"namespace":        InstanceNamespaceName(inst),
+			"nesting":          shared.IsTrue(inst.ExpandedConfig()["security.nesting"]),
+			"raw":              rawContent,
+			"unprivileged":     !shared.IsTrue(inst.ExpandedConfig()["security.privileged"]) || state.OS.RunningInUserNS,
+		})
+		if err != nil {
+			return "", err
+		}
+	} else {
+		rootPath := ""
+		if shared.InSnap() {
+			rootPath = "/var/lib/snapd/hostfs"
+		}
+
+		// AppArmor requires deref of all paths.
+		path, err := filepath.EvalSymlinks(inst.Path())
+		if err != nil {
+			return "", err
+		}
+
+		devPaths := inst.DevPaths()
+		for i := range devPaths {
+			devPaths[i], err = filepath.EvalSymlinks(devPaths[i])
+			if err != nil {
+				return "", err
+			}
+		}
+
+		err = qemuProfileTpl.Execute(sb, map[string]interface{}{
+			"devPaths":    inst.DevPaths(),
+			"exePath":     util.GetExecPath(),
+			"libraryPath": strings.Split(os.Getenv("LD_LIBRARY_PATH"), ":"),
+			"logPath":     inst.LogPath(),
+			"name":        InstanceProfileName(inst),
+			"path":        path,
+			"raw":         rawContent,
+			"rootPath":    rootPath,
+			"snap":        shared.InSnap(),
+		})
+		if err != nil {
+			return "", err
+		}
 	}
 
 	return sb.String(), nil
diff --git a/lxd/apparmor/instance_qemu.go b/lxd/apparmor/instance_qemu.go
new file mode 100644
index 0000000000..7ceb02dc52
--- /dev/null
+++ b/lxd/apparmor/instance_qemu.go
@@ -0,0 +1,74 @@
+package apparmor
+
+import (
+	"text/template"
+)
+
+var qemuProfileTpl = template.Must(template.New("qemuProfile").Parse(`#include <tunables/global>
+profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) {
+  #include <abstractions/base>
+  #include <abstractions/consoles>
+  #include <abstractions/nameservice>
+
+  capability dac_override,
+  capability dac_read_search,
+  capability setgid,
+  capability setuid,
+  capability sys_chroot,
+  capability sys_resource,
+
+  # Needed by qemu
+  /{,usr/}bin/qemu*                         mrix,
+  /dev/hugepages/**                         w,
+  /dev/kvm                                  w,
+  /dev/net/tun                              w,
+  /dev/ptmx                                 w,
+  /dev/vfio/**                              w,
+  /dev/vhost-net                            w,
+  /dev/vhost-vsock                          w,
+  /etc/ceph/**                              r,
+  /usr/share/OVMF/OVMF_CODE.fd              kr,
+  owner @{PROC}/@{pid}/task/@{tid}/comm     rw,
+
+  # Instance specific paths
+  {{ .logPath }}/** rwk,
+  {{ .path }}/qemu.nvram rwk,
+{{range $index, $element := .devPaths}}
+  {{$element}} rwk,
+{{- end }}
+
+  # Needed for lxd fork commands
+  {{ .exePath }} mr,
+  @{PROC}/@{pid}/cmdline r,
+  {{ .rootPath }}/{etc,lib,usr/lib}/os-release r,
+
+  # Things that we definitely don't need
+  deny @{PROC}/@{pid}/cgroup r,
+  deny /sys/module/apparmor/parameters/enabled r,
+  deny /sys/kernel/mm/transparent_hugepage/hpage_pmd_size r,
+
+{{- if .snap }}
+  # The binary itself (for nesting)
+  /var/snap/lxd/common/lxd.debug            mr,
+  /snap/lxd/*/bin/lxd                       mr,
+  /snap/lxd/*/bin/qemu*                     mrix,
+  /snap/lxd/*/share/qemu/OVMF_CODE.fd       kr,
+
+  # Snap-specific libraries
+  /snap/lxd/*/lib/**.so*            mr,
+{{- end }}
+
+{{if .libraryPath -}}
+  # Entries from LD_LIBRARY_PATH
+{{range $index, $element := .libraryPath}}
+  {{$element}}/** mr,
+{{- end }}
+{{- end }}
+
+{{- if .raw }}
+
+  ### Configuration: raw.apparmor
+{{ .raw }}
+{{- end }}
+}
+`))
diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go
index 6a5f7cd2f7..fc1a8ac5cc 100644
--- a/lxd/instance/drivers/driver_qemu.go
+++ b/lxd/instance/drivers/driver_qemu.go
@@ -27,6 +27,7 @@ import (
 	"gopkg.in/yaml.v2"
 
 	lxdClient "github.com/lxc/lxd/client"
+	"github.com/lxc/lxd/lxd/apparmor"
 	"github.com/lxc/lxd/lxd/backup"
 	"github.com/lxc/lxd/lxd/cluster"
 	"github.com/lxc/lxd/lxd/db"
@@ -505,6 +506,12 @@ func (vm *qemu) Freeze() error {
 
 // onStop is run when the instance stops.
 func (vm *qemu) onStop(target string) error {
+	ctxMap := log.Ctx{
+		"project":   vm.project,
+		"name":      vm.name,
+		"ephemeral": vm.ephemeral,
+	}
+
 	// Pick up the existing stop operation lock created in Stop() function.
 	op := operationlock.Get(vm.id)
 	if op != nil && op.Action() != "stop" {
@@ -526,6 +533,13 @@ func (vm *qemu) onStop(target string) error {
 		return err
 	}
 
+	// Unload the apparmor profile
+	err = apparmor.InstanceUnload(vm.state, vm)
+	if err != nil {
+		ctxMap["err"] = err
+		logger.Error("Failed to unload AppArmor profile", ctxMap)
+	}
+
 	if target == "reboot" {
 		err = vm.Start(false)
 	} else if vm.ephemeral {
@@ -837,6 +851,15 @@ func (vm *qemu) Start(stateful bool) error {
 		return err
 	}
 
+	// Load the AppArmor profile
+	err = apparmor.InstanceLoad(vm.state, vm)
+	if err != nil {
+		op.Done(err)
+		return err
+	}
+
+	p.SetApparmor(apparmor.InstanceProfileName(vm))
+
 	// Open any extra files and pass their file handles to qemu command.
 	files := []*os.File{}
 	for _, file := range fdFiles {
@@ -3230,6 +3253,9 @@ func (vm *qemu) cleanup() {
 	vm.removeUnixDevices()
 	vm.removeDiskDevices()
 
+	// Remove the security profiles
+	apparmor.InstanceDelete(vm.state, vm)
+
 	// Remove the devices path
 	os.Remove(vm.DevicesPath())
 


More information about the lxc-devel mailing list