[lxc-devel] [lxd/master] Add apparmor confinement for forkproxy

stgraber on Github lxc-bot at linuxcontainers.org
Wed Aug 26 22:37:18 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/20200826/3845ff77/attachment-0001.bin>
-------------- next part --------------
From 8b6110431401fbfd81c8ab7f6aa403f397b3cc84 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 26 Aug 2020 18:30:48 -0400
Subject: [PATCH 1/4] lxd/apparmor/dnsmasq: Add /proc/self/fd
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_dnsmasq.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lxd/apparmor/network_dnsmasq.go b/lxd/apparmor/network_dnsmasq.go
index 7fec180d52..ef2c5ef691 100644
--- a/lxd/apparmor/network_dnsmasq.go
+++ b/lxd/apparmor/network_dnsmasq.go
@@ -36,6 +36,7 @@ profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) {
 
   # Additional system files
   @{PROC}/sys/net/ipv6/conf/*/mtu r,
+  @{PROC}/@{pid}/fd/ r,
 
   # System configuration access
   {{ .rootPath }}/etc/gai.conf           r,

From 5d3a3571534105bee5b4537b9a4d635dae19d73e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Wed, 26 Aug 2020 18:34:14 -0400
Subject: [PATCH 2/4] lxd/apparmor/forkdns: Allow reading/mapping the binary
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_forkdns.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/lxd/apparmor/network_forkdns.go b/lxd/apparmor/network_forkdns.go
index b9e66f9791..bc5dfecfc9 100644
--- a/lxd/apparmor/network_forkdns.go
+++ b/lxd/apparmor/network_forkdns.go
@@ -7,6 +7,7 @@ import (
 	"text/template"
 
 	"github.com/lxc/lxd/lxd/state"
+	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
 )
 
@@ -26,6 +27,7 @@ profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) {
   {{ .varPath }}/networks/{{ .networkName }}/forkdns.servers/servers.conf r,
 
   # Needed for lxd fork commands
+  {{ .exePath }} mr,
   @{PROC}/@{pid}/cmdline r,
   {{ .rootPath }}/{etc,lib,usr/lib}/os-release r,
 
@@ -68,6 +70,7 @@ func forkdnsProfile(state *state.State, n network) (string, error) {
 		"rootPath":    rootPath,
 		"snap":        shared.InSnap(),
 		"libraryPath": strings.Split(os.Getenv("LD_LIBRARY_PATH"), ":"),
+		"exePath":     util.GetExecPath(),
 	})
 	if err != nil {
 		return "", err

From 8dad7747ae6b711703c851ae5d6e9d8163668cec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 25 Aug 2020 23:05:40 -0400
Subject: [PATCH 3/4] lxd/apparmor: Add forkproxy
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_forkproxy.go | 153 +++++++++++++++++++++++++++++
 1 file changed, 153 insertions(+)
 create mode 100644 lxd/apparmor/instance_forkproxy.go

diff --git a/lxd/apparmor/instance_forkproxy.go b/lxd/apparmor/instance_forkproxy.go
new file mode 100644
index 0000000000..6625c4b0a2
--- /dev/null
+++ b/lxd/apparmor/instance_forkproxy.go
@@ -0,0 +1,153 @@
+package apparmor
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"text/template"
+
+	"github.com/lxc/lxd/lxd/project"
+	"github.com/lxc/lxd/lxd/state"
+	"github.com/lxc/lxd/shared"
+)
+
+// Internal copy of the device interface.
+type device interface {
+	Name() string
+}
+
+var forkproxyProfileTpl = template.Must(template.New("forkproxyProfile").Parse(`#include <tunables/global>
+profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) {
+  #include <abstractions/base>
+
+  # Capabilities
+  capability dac_read_search,
+  capability dac_override,
+  capability kill,
+  capability net_bind_service,
+  capability sys_admin,
+  capability sys_ptrace,
+
+  # Network access
+  network inet dgram,
+  network inet6 dgram,
+
+  # Forkproxy operation
+  @{PROC}/** rw,
+  / rw,
+  ptrace (read),
+
+  # Needed for lxd fork commands
+  @{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-specific libraries
+  /snap/lxd/*/lib/**.so*              mr,
+{{- end }}
+
+{{if .libraryPath -}}
+  # Entries from LD_LIBRARY_PATH
+{{range $index, $element := .libraryPath}}
+  {{$element}}/** mr,
+{{- end }}
+{{- end }}
+}
+`))
+
+// forkproxyProfile generates the AppArmor profile template from the given network.
+func forkproxyProfile(state *state.State, inst instance, dev device) (string, error) {
+	rootPath := ""
+	if shared.InSnap() {
+		rootPath = "/var/lib/snapd/hostfs"
+	}
+
+	// Render the profile.
+	var sb *strings.Builder = &strings.Builder{}
+	err := forkproxyProfileTpl.Execute(sb, map[string]interface{}{
+		"name":        ForkproxyProfileName(inst, dev),
+		"varPath":     shared.VarPath(""),
+		"rootPath":    rootPath,
+		"snap":        shared.InSnap(),
+		"libraryPath": strings.Split(os.Getenv("LD_LIBRARY_PATH"), ":"),
+	})
+	if err != nil {
+		return "", err
+	}
+
+	return sb.String(), nil
+}
+
+// ForkproxyProfileName returns the AppArmor profile name.
+func ForkproxyProfileName(inst instance, dev device) string {
+	path := shared.VarPath("")
+	name := fmt.Sprintf("%s_%s_<%s>", dev.Name(), project.Instance(inst.Project(), inst.Name()), path)
+	return profileName("", name)
+}
+
+// forkproxyProfileFilename returns the name of the on-disk profile name.
+func forkproxyProfileFilename(inst instance, dev device) string {
+	name := fmt.Sprintf("%s_%s", dev.Name(), project.Instance(inst.Project(), inst.Name()))
+	return profileName("forkproxy", name)
+}
+
+// ForkproxyLoad ensures that the instances's policy is loaded into the kernel so the it can boot.
+func ForkproxyLoad(state *state.State, inst instance, dev device) error {
+	/* In order to avoid forcing a profile parse (potentially slow) on
+	 * every container 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", forkproxyProfileFilename(inst, dev))
+	content, err := ioutil.ReadFile(profile)
+	if err != nil && !os.IsNotExist(err) {
+		return err
+	}
+
+	updated, err := forkproxyProfile(state, inst, dev)
+	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, forkproxyProfileFilename(inst, dev))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 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))
+}
+
+// ForkproxyDelete removes the policy from cache/disk.
+func ForkproxyDelete(state *state.State, inst instance, dev device) error {
+	return deleteProfile(state, forkproxyProfileFilename(inst, dev))
+}

From 519ea926437a69978ac8b2a5aadd8fd66ff6dde2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber at ubuntu.com>
Date: Tue, 25 Aug 2020 23:05:48 -0400
Subject: [PATCH 4/4] lxd/device/forkproxy: Add apparmor
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_forkproxy.go | 40 ++++++++++++++++++++++++++++++
 lxd/device/proxy.go                | 25 +++++++++++++++++++
 2 files changed, 65 insertions(+)

diff --git a/lxd/apparmor/instance_forkproxy.go b/lxd/apparmor/instance_forkproxy.go
index 6625c4b0a2..ca3f4a2288 100644
--- a/lxd/apparmor/instance_forkproxy.go
+++ b/lxd/apparmor/instance_forkproxy.go
@@ -8,13 +8,16 @@ import (
 	"strings"
 	"text/template"
 
+	deviceConfig "github.com/lxc/lxd/lxd/device/config"
 	"github.com/lxc/lxd/lxd/project"
 	"github.com/lxc/lxd/lxd/state"
+	"github.com/lxc/lxd/lxd/util"
 	"github.com/lxc/lxd/shared"
 )
 
 // Internal copy of the device interface.
 type device interface {
+	Config() deviceConfig.Device
 	Name() string
 }
 
@@ -23,16 +26,24 @@ profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) {
   #include <abstractions/base>
 
   # Capabilities
+  capability chown,
   capability dac_read_search,
   capability dac_override,
+  capability fowner,
+  capability fsetid,
   capability kill,
   capability net_bind_service,
+  capability setgid,
+  capability setuid,
   capability sys_admin,
+  capability sys_chroot,
   capability sys_ptrace,
 
   # Network access
   network inet dgram,
   network inet6 dgram,
+  network inet stream,
+  network inet6 stream,
 
   # Forkproxy operation
   @{PROC}/** rw,
@@ -40,8 +51,14 @@ profile "{{ .name }}" flags=(attach_disconnected,mediate_deleted) {
   ptrace (read),
 
   # Needed for lxd fork commands
+  {{ .exePath }} mr,
   @{PROC}/@{pid}/cmdline r,
   {{ .rootPath }}/{etc,lib,usr/lib}/os-release r,
+{{if .sockets -}}
+{{range $index, $element := .sockets}}
+  {{$element}} rw,
+{{- end }}
+{{- end }}
 
   # Things that we definitely don't need
   deny @{PROC}/@{pid}/cgroup r,
@@ -73,6 +90,27 @@ func forkproxyProfile(state *state.State, inst instance, dev device) (string, er
 		rootPath = "/var/lib/snapd/hostfs"
 	}
 
+	// Add any socket used by forkproxy.
+	sockets := []string{}
+
+	fields := strings.SplitN(dev.Config()["listen"], ":", 2)
+	if fields[0] == "unix" && !strings.HasPrefix(fields[1], "@") {
+		if dev.Config()["bind"] == "host" || dev.Config()["bind"] == "" {
+			sockets = append(sockets, shared.HostPath(fields[1]))
+		} else {
+			sockets = append(sockets, fields[1])
+		}
+	}
+
+	fields = strings.SplitN(dev.Config()["connect"], ":", 2)
+	if fields[0] == "unix" && !strings.HasPrefix(fields[1], "@") {
+		if dev.Config()["bind"] == "host" || dev.Config()["bind"] == "" {
+			sockets = append(sockets, fields[1])
+		} else {
+			sockets = append(sockets, shared.HostPath(fields[1]))
+		}
+	}
+
 	// Render the profile.
 	var sb *strings.Builder = &strings.Builder{}
 	err := forkproxyProfileTpl.Execute(sb, map[string]interface{}{
@@ -80,7 +118,9 @@ func forkproxyProfile(state *state.State, inst instance, dev device) (string, er
 		"varPath":     shared.VarPath(""),
 		"rootPath":    rootPath,
 		"snap":        shared.InSnap(),
+		"exePath":     util.GetExecPath(),
 		"libraryPath": strings.Split(os.Getenv("LD_LIBRARY_PATH"), ":"),
+		"sockets":     sockets,
 	})
 	if err != nil {
 		return "", err
diff --git a/lxd/device/proxy.go b/lxd/device/proxy.go
index 9d3420622e..80224ed23d 100644
--- a/lxd/device/proxy.go
+++ b/lxd/device/proxy.go
@@ -13,6 +13,7 @@ import (
 	"github.com/pkg/errors"
 	liblxc "gopkg.in/lxc/go-lxc.v2"
 
+	"github.com/lxc/lxd/lxd/apparmor"
 	deviceConfig "github.com/lxc/lxd/lxd/device/config"
 	"github.com/lxc/lxd/lxd/device/nictype"
 	"github.com/lxc/lxd/lxd/instance"
@@ -193,6 +194,12 @@ func (d *proxy) Start() (*deviceConfig.RunConfig, error) {
 			logFileName := fmt.Sprintf("proxy.%s.log", d.name)
 			logPath := filepath.Join(d.inst.LogPath(), logFileName)
 
+			// Load the apparmor profile
+			err = apparmor.ForkproxyLoad(d.state, d.inst, d)
+			if err != nil {
+				return err
+			}
+
 			// Spawn the daemon using subprocess
 			command := d.state.OS.ExecPath
 			forkproxyargs := []string{"forkproxy",
@@ -216,6 +223,8 @@ func (d *proxy) Start() (*deviceConfig.RunConfig, error) {
 				return fmt.Errorf("Failed to create subprocess: %s", err)
 			}
 
+			p.SetApparmor(apparmor.ForkproxyProfileName(d.inst, d))
+
 			err = p.StartWithFiles(proxyValues.inheritFds)
 			if err != nil {
 				return fmt.Errorf("Failed to run: %s %s: %v", command, strings.Join(forkproxyargs, " "), err)
@@ -309,6 +318,12 @@ func (d *proxy) Stop() (*deviceConfig.RunConfig, error) {
 		return nil, err
 	}
 
+	// Unload apparmor profile.
+	err = apparmor.ForkproxyUnload(d.state, d.inst, d)
+	if err != nil {
+		return nil, err
+	}
+
 	return nil, nil
 }
 
@@ -545,3 +560,13 @@ func (d *proxy) killProxyProc(pidPath string) error {
 	os.Remove(pidPath)
 	return nil
 }
+
+func (d *proxy) Remove() error {
+	// Delete apparmor profile.
+	err := apparmor.ForkproxyDelete(d.state, d.inst, d)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}


More information about the lxc-devel mailing list