[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