[lxc-devel] [lxd/master] AppArmor confinement of dnsmasq

stgraber on Github lxc-bot at linuxcontainers.org
Fri Jul 17 20:42:16 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/20200717/e9a1c151/attachment.bin>
-------------- next part --------------
From 6d77c41a8a8e26f868a4f9849213d69547cbd1d2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 17 Jul 2020 15:06:30 -0400
Subject: [PATCH 1/5] shared: Add InSnap
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/util.go | 20 ++++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/shared/util.go b/shared/util.go
index bfb217d400..74664c07b8 100644
--- a/shared/util.go
+++ b/shared/util.go
@@ -120,9 +120,7 @@ func HostPathFollow(path string) string {
 	}
 
 	// Check if we're running in a snap package.
-	_, inSnap := os.LookupEnv("SNAP")
-	snapName := os.Getenv("SNAP_NAME")
-	if !inSnap || snapName != "lxd" {
+	if !InSnap() {
 		return path
 	}
 
@@ -174,9 +172,7 @@ func HostPath(path string) string {
 	}
 
 	// Check if we're running in a snap package
-	_, inSnap := os.LookupEnv("SNAP")
-	snapName := os.Getenv("SNAP_NAME")
-	if !inSnap || snapName != "lxd" {
+	if !InSnap() {
 		return path
 	}
 
@@ -1219,3 +1215,15 @@ func GetSnapshotExpiry(refDate time.Time, s string) (time.Time, error) {
 
 	return t, nil
 }
+
+// InSnap returns true if we're running inside the LXD snap.
+func InSnap() bool {
+	// Detect the snap.
+	_, snapPath := os.LookupEnv("SNAP")
+	snapName := os.Getenv("SNAP_NAME")
+	if snapPath && snapName == "lxd" {
+		return true
+	}
+
+	return false
+}

From a184a0334fa123dc8878e3c74b9c4b5f32e8f3de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 17 Jul 2020 16:34:18 -0400
Subject: [PATCH 2/5] shared/subprocess: Add AppArmor support
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 | 39 +++++++++++++++++++++++++++++++++------
 1 file changed, 33 insertions(+), 6 deletions(-)

diff --git a/shared/subprocess/proc.go b/shared/subprocess/proc.go
index 2ca1eca9a9..45a02cddf3 100644
--- a/shared/subprocess/proc.go
+++ b/shared/subprocess/proc.go
@@ -10,6 +10,8 @@ import (
 
 	"github.com/pkg/errors"
 	"gopkg.in/yaml.v2"
+
+	"github.com/lxc/lxd/shared"
 )
 
 // Process struct. Has ability to set runtime arguments
@@ -20,11 +22,25 @@ type Process struct {
 	chExit     chan struct{} `yaml:"-"`
 	hasMonitor bool          `yaml:"-"`
 
-	Name   string   `yaml:"name"`
-	Args   []string `yaml:"args,flow"`
-	Pid    int64    `yaml:"pid"`
-	Stdout string   `yaml:"stdout"`
-	Stderr string   `yaml:"stderr"`
+	Name     string   `yaml:"name"`
+	Args     []string `yaml:"args,flow"`
+	Apparmor string   `yaml:"apparmor"`
+	Pid      int64    `yaml:"pid"`
+	Stdout   string   `yaml:"stdout"`
+	Stderr   string   `yaml:"stderr"`
+}
+
+func (p *Process) hasApparmor() bool {
+	_, err := exec.LookPath("aa-exec")
+	if err != nil {
+		return false
+	}
+
+	if !shared.PathExists("/sys/kernel/security/apparmor") {
+		return false
+	}
+
+	return true
 }
 
 // GetPid returns the pid for the given process object
@@ -38,6 +54,11 @@ func (p *Process) GetPid() (int64, error) {
 	return 0, ErrNotRunning
 }
 
+// SetApparmor allows setting the AppArmor profile.
+func (p *Process) SetApparmor(profile string) {
+	p.Apparmor = profile
+}
+
 // Stop will stop the given process object
 func (p *Process) Stop() error {
 	pr, _ := os.FindProcess(int(p.Pid))
@@ -69,7 +90,13 @@ func (p *Process) Stop() error {
 
 // Start will start the given process object
 func (p *Process) Start() error {
-	cmd := exec.Command(p.Name, p.Args...)
+	var cmd *exec.Cmd
+
+	if p.Apparmor != "" && p.hasApparmor() {
+		cmd = exec.Command("aa-exec", append([]string{"-p", p.Apparmor, p.Name}, p.Args...)...)
+	} else {
+		cmd = exec.Command(p.Name, p.Args...)
+	}
 	cmd.Stdin = nil
 	cmd.SysProcAttr = &syscall.SysProcAttr{}
 	cmd.SysProcAttr.Setsid = true

From c92613b9c1af976c96ef502b9704efbdd759e342 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 17 Jul 2020 15:21:55 -0400
Subject: [PATCH 3/5] lxd/apparmor: Rename template
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     | 2 +-
 lxd/apparmor/instance_lxc.go | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lxd/apparmor/instance.go b/lxd/apparmor/instance.go
index 9bdd93fb62..ebb3ff4f25 100644
--- a/lxd/apparmor/instance.go
+++ b/lxd/apparmor/instance.go
@@ -157,7 +157,7 @@ func instanceProfile(state *state.State, inst instance) (string, error) {
 
 	// Render the profile.
 	var sb *strings.Builder = &strings.Builder{}
-	err = lxcProfile.Execute(sb, map[string]interface{}{
+	err = lxcProfileTpl.Execute(sb, map[string]interface{}{
 		"feature_unix":     unixSupported,
 		"feature_cgns":     shared.PathExists("/proc/self/ns/cgroup"),
 		"feature_stacking": state.OS.AppArmorStacking && !state.OS.AppArmorStacked,
diff --git a/lxd/apparmor/instance_lxc.go b/lxd/apparmor/instance_lxc.go
index 4d6c423b54..3962e4f0fb 100644
--- a/lxd/apparmor/instance_lxc.go
+++ b/lxd/apparmor/instance_lxc.go
@@ -4,7 +4,7 @@ import (
 	"text/template"
 )
 
-var lxcProfile = template.Must(template.New("lxcProfile").Parse(`#include <tunables/global>
+var lxcProfileTpl = template.Must(template.New("lxcProfile").Parse(`#include <tunables/global>
 profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) {
   ### Base profile
   capability,

From 19b80ae5caa8f29de7e3a07d8475c783592b905d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 17 Jul 2020 15:22:50 -0400
Subject: [PATCH 4/5] lxd/apparmor: Add dnsmasq 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/network.go         | 126 ++++++++++++++++++++++++++++++++
 lxd/apparmor/network_dnsmasq.go |  57 +++++++++++++++
 2 files changed, 183 insertions(+)
 create mode 100644 lxd/apparmor/network.go
 create mode 100644 lxd/apparmor/network_dnsmasq.go

diff --git a/lxd/apparmor/network.go b/lxd/apparmor/network.go
new file mode 100644
index 0000000000..e3615c6812
--- /dev/null
+++ b/lxd/apparmor/network.go
@@ -0,0 +1,126 @@
+package apparmor
+
+import (
+	"crypto/sha256"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/lxc/lxd/lxd/state"
+	"github.com/lxc/lxd/shared"
+)
+
+// Internal copy of the network interface.
+type network interface {
+	Name() string
+}
+
+// DnsmasqProfileName returns the AppArmor profile name.
+func DnsmasqProfileName(n network) string {
+	path := shared.VarPath("")
+	name := fmt.Sprintf("%s_<%s>", n.Name(), path)
+
+	// Max length in AppArmor is 253 chars.
+	if len(name)+12 >= 253 {
+		hash := sha256.New()
+		io.WriteString(hash, name)
+		name = fmt.Sprintf("%x", hash.Sum(nil))
+	}
+
+	return fmt.Sprintf("lxd_dnsmasq-%s", name)
+}
+
+// dnsmasqProfileFilename returns the name of the on-disk profile name.
+func dnsmasqProfileFilename(n network) string {
+	name := n.Name()
+
+	// Max length in AppArmor is 253 chars.
+	if len(name)+12 >= 253 {
+		hash := sha256.New()
+		io.WriteString(hash, name)
+		name = fmt.Sprintf("%x", hash.Sum(nil))
+	}
+
+	return fmt.Sprintf("lxd_dnsmasq-%s", name)
+}
+
+// NetworkLoad ensures that the network's profiles are loaded into the kernel.
+func NetworkLoad(state *state.State, n network) error {
+	/* In order to avoid forcing a profile parse (potentially slow) on
+	 * every network start, let's use AppArmor's binary policy cache,
+	 * which checks mtime of the files to figure out if the policy needs to
+	 * be regenerated.
+	 *
+	 * Since it uses mtimes, we shouldn't just always write out our local
+	 * AppArmor template; instead we should check to see whether the
+	 * template is the same as ours. If it isn't we should write our
+	 * version out so that the new changes are reflected and we definitely
+	 * force a recompile.
+	 */
+	profile := filepath.Join(aaPath, "profiles", dnsmasqProfileFilename(n))
+	content, err := ioutil.ReadFile(profile)
+	if err != nil && !os.IsNotExist(err) {
+		return err
+	}
+
+	updated, err := dnsmasqProfile(state, n)
+	if err != nil {
+		return err
+	}
+
+	if string(content) != string(updated) {
+		err = ioutil.WriteFile(profile, []byte(updated), 0600)
+		if err != nil {
+			return err
+		}
+	}
+
+	err = loadProfile(state, dnsmasqProfileFilename(n))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// NetworkUnload ensures that the network's profiles are unloaded to free kernel memory.
+// This does not delete the policy from disk or cache.
+func NetworkUnload(state *state.State, n network) error {
+	err := unloadProfile(state, dnsmasqProfileFilename(n))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// NetworkDelete removes the profiles from cache/disk.
+func NetworkDelete(state *state.State, n network) error {
+	return deleteProfile(state, dnsmasqProfileFilename(n))
+}
+
+// dnsmasqProfile generates the AppArmor profile template from the given network.
+func dnsmasqProfile(state *state.State, n network) (string, error) {
+	rootPath := ""
+	if shared.InSnap() {
+		rootPath = "/var/lib/snapd/hostfs"
+	}
+
+	// Render the profile.
+	var sb *strings.Builder = &strings.Builder{}
+	err := dnsmasqProfileTpl.Execute(sb, map[string]interface{}{
+		"name":        DnsmasqProfileName(n),
+		"networkName": n.Name(),
+		"varPath":     shared.VarPath(""),
+		"rootPath":    rootPath,
+		"snap":        shared.InSnap(),
+	})
+	if err != nil {
+		return "", err
+	}
+
+	return sb.String(), nil
+}
diff --git a/lxd/apparmor/network_dnsmasq.go b/lxd/apparmor/network_dnsmasq.go
new file mode 100644
index 0000000000..ca8566dab4
--- /dev/null
+++ b/lxd/apparmor/network_dnsmasq.go
@@ -0,0 +1,57 @@
+package apparmor
+
+import (
+	"text/template"
+)
+
+var dnsmasqProfileTpl = template.Must(template.New("dnsmasqProfile").Parse(`#include <tunables/global>
+profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) {
+  #include <abstractions/base>
+  #include <abstractions/dbus>
+  #include <abstractions/nameservice>
+
+  # Capabilities
+  capability chown,
+  capability net_bind_service,
+  capability setgid,
+  capability setuid,
+  capability dac_override,
+  capability dac_read_search,
+  capability net_admin,         # for DHCP server
+  capability net_raw,           # for DHCP server ping checks
+
+  # Network access
+  network inet raw,
+  network inet6 raw,
+
+  # Network-specific paths
+  {{ .varPath }}/networks/{{ .networkName }}/dnsmasq.hosts/{,*} r,
+  {{ .varPath }}/networks/{{ .networkName }}/dnsmasq.leases rw,
+  {{ .varPath }}/networks/{{ .networkName }}/dnsmasq.raw r,
+
+  # Additional system files
+  @{PROC}/sys/net/ipv6/conf/*/mtu r,
+
+  # System configuration access
+  {{ .rootPath }}/etc/gai.conf           r,
+  {{ .rootPath }}/etc/group              r,
+  {{ .rootPath }}/etc/host.conf          r,
+  {{ .rootPath }}/etc/hosts              r,
+  {{ .rootPath }}/etc/nsswitch.conf      r,
+  {{ .rootPath }}/etc/passwd             r,
+  {{ .rootPath }}/etc/protocols          r,
+
+  {{ .rootPath }}/etc/resolv.conf        r,
+  {{ .rootPath }}/etc/resolvconf/run/resolv.conf r,
+
+  {{ .rootPath }}/run/{resolvconf,NetworkManager,systemd/resolve,connman,netconfig}/resolv.conf r,
+  {{ .rootPath }}/run/systemd/resolve/stub-resolv.conf r,
+
+{{- if .snap }}
+
+  # Snap-specific libraries
+  /snap/lxd/current/lib/**.so*            mr,
+  /snap/lxd/*/lib/**.so*                  mr,
+{{- end }}
+}
+`))

From 1ee95722e180d35e94ecf8fd2f09f38fe3fb38fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Fri, 17 Jul 2020 16:34:57 -0400
Subject: [PATCH 5/5] lxd/networks: Use AppArmor when available
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/network/driver_bridge.go | 32 ++++++++++++++++++++++++++++++--
 1 file changed, 30 insertions(+), 2 deletions(-)

diff --git a/lxd/network/driver_bridge.go b/lxd/network/driver_bridge.go
index fdf854ef75..3a1612029d 100644
--- a/lxd/network/driver_bridge.go
+++ b/lxd/network/driver_bridge.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/pkg/errors"
 
+	"github.com/lxc/lxd/lxd/apparmor"
 	"github.com/lxc/lxd/lxd/cluster"
 	"github.com/lxc/lxd/lxd/daemon"
 	"github.com/lxc/lxd/lxd/dnsmasq"
@@ -282,6 +283,12 @@ func (n *bridge) Delete(clusterNotification bool) error {
 		}
 	}
 
+	// Delete apparmor profiles.
+	err := apparmor.NetworkDelete(n.state, n)
+	if err != nil {
+		return err
+	}
+
 	return n.common.delete(clusterNotification)
 }
 
@@ -571,7 +578,8 @@ func (n *bridge) setup(oldConfig map[string]string) error {
 	command := "dnsmasq"
 	dnsmasqCmd := []string{"--keep-in-foreground", "--strict-order", "--bind-interfaces",
 		"--except-interface=lo",
-		"--no-ping", // --no-ping is very important to prevent delays to lease file updates.
+		"--pid-file=", // Disable attempt at writing a PID file.
+		"--no-ping",   // --no-ping is very important to prevent delays to lease file updates.
 		fmt.Sprintf("--interface=%s", n.name)}
 
 	dnsmasqVersion, err := dnsmasq.GetVersion()
@@ -1107,6 +1115,12 @@ func (n *bridge) setup(oldConfig map[string]string) error {
 		}
 	}
 
+	// Generate and load apparmor profiles.
+	err = apparmor.NetworkLoad(n.state, n)
+	if err != nil {
+		return err
+	}
+
 	// Kill any existing dnsmasq and forkdns daemon for this network
 	err = dnsmasq.Kill(n.name, false)
 	if err != nil {
@@ -1168,12 +1182,20 @@ func (n *bridge) setup(oldConfig map[string]string) error {
 			return err
 		}
 
-		// Create subprocess object dnsmasq (occasionally races, try a few times)
+		// Create subprocess object dnsmasq.
 		p, err := subprocess.NewProcess(command, dnsmasqCmd, "", "")
 		if err != nil {
 			return fmt.Errorf("Failed to create subprocess: %s", err)
 		}
 
+		// Apply AppArmor confinement.
+		if n.config["raw.dnsmasq"] == "" {
+			p.SetApparmor(apparmor.DnsmasqProfileName(n))
+		} else {
+			n.logger.Warn("Skipping AppArmor for dnsmasq due to raw.dnsmasq being set", log.Ctx{"name": n.name})
+		}
+
+		// Start dnsmasq.
 		err = p.Start()
 		if err != nil {
 			return fmt.Errorf("Failed to run: %s %s: %v", command, strings.Join(dnsmasqCmd, " "), err)
@@ -1296,6 +1318,12 @@ func (n *bridge) Stop() error {
 		}
 	}
 
+	// Unload apparmor profiles.
+	err = apparmor.NetworkUnload(n.state, n)
+	if err != nil {
+		return err
+	}
+
 	return nil
 }
 


More information about the lxc-devel mailing list