Skip to content

Commit

Permalink
i/apparmor: support for home.d tunables from /etc/ (snapcore#13118)
Browse files Browse the repository at this point in the history
* i/apparmor: support for home.d tunables from /etc/

* tests: update snapd-homedirs-vendored to run on all ubuntu versions

* i/apparmor: add additional unit test

Only enable the spread test for ubuntu 20 and newer as any distro before don't support the neccessary features

---------

Co-authored-by: Michael Vogt <mvo@ubuntu.com>
  • Loading branch information
Meulengracht and mvo5 committed Aug 25, 2023
1 parent dcb8ad2 commit b98e4af
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 0 deletions.
22 changes: 22 additions & 0 deletions interfaces/apparmor/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ func snapConfineFromSnapProfile(info *snap.Info) (dir, glob string, content map[
patchedProfileText = bytes.Replace(
patchedProfileText, []byte("/var/lib/snapd/apparmor/snap-confine"), []byte(apparmor_sandbox.SnapConfineAppArmorDir), -1)

// To support non standard homedirs we currently use the home.d tunables, which are
// written to the system apparmor directory. However snapd vendors its own apparmor, which
// uses the readonly filesystem, which we cannot modify with our own snippets. So we force
// include the home.d tunables from /etc if necessary.
// We should be safely able to use "#include if exists" as the vendored apparmor supports this.
// XXX: Replace include home tunables until we have a better solution
features, _ := parserFeatures()
if strutil.ListContains(features, "snapd-internal") {
patchedProfileText = bytes.Replace(
patchedProfileText,
[]byte("#include <tunables/global>"),
[]byte("#include <tunables/global>\n#include if exists \"/etc/apparmor.d/tunables/home.d/\""), -1)
}

// Also replace the test providing access to verbatim
// /usr/lib/snapd/snap-confine, which is necessary because to execute snaps
// from strict snaps, we need to be able read and map
Expand Down Expand Up @@ -829,6 +843,14 @@ func (b *Backend) addContent(securityTag string, snapInfo *snap.Info, cmdName st
return `#include if exists "/var/lib/snapd/apparmor/snap-tuning"`
}
return ""
// XXX: Remove this when we have a better solution to including the system
// tunables. See snapConfineFromSnapProfile() for a more detailed explanation.
case "###INCLUDE_SYSTEM_TUNABLES_HOME_D_WITH_VENDORED_APPARMOR###":
features, _ := parserFeatures()
if strutil.ListContains(features, "snapd-internal") {
return `#include if exists "/etc/apparmor.d/tunables/home.d"`
}
return ""
case "###VAR###":
return templateVariables(snapInfo, securityTag, cmdName)
case "###PROFILEATTACH###":
Expand Down
68 changes: 68 additions & 0 deletions interfaces/apparmor/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,40 @@ func (s *backendSuite) TestCombineSnippetsIncludeIfExists(c *C) {
}
}

func (s *backendSuite) TestCombineSnippetsIncludeEtcTunables(c *C) {
restore := apparmor_sandbox.MockLevel(apparmor_sandbox.Full)
defer restore()

restoreTemplate := apparmor.MockTemplate("###INCLUDE_SYSTEM_TUNABLES_HOME_D_WITH_VENDORED_APPARMOR###")
defer restoreTemplate()

type includeIfExistsScenario struct {
features []string
expected string
}

var includeIfExistsScenarios = []includeIfExistsScenario{{
features: []string{},
expected: "",
}, {
features: []string{"snapd-internal"},
expected: `#include if exists "/etc/apparmor.d/tunables/home.d"`,
}}

for i, scenario := range includeIfExistsScenarios {
restore = apparmor.MockParserFeatures(func() ([]string, error) { return scenario.features, nil })
defer restore()

snapInfo := s.InstallSnap(c, interfaces.ConfinementOptions{}, "", ifacetest.SambaYamlV1, 1)
profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd")
c.Check(profile, testutil.FileEquals, scenario.expected, Commentf("scenario %d: %#v", i, scenario))
stat, err := os.Stat(profile)
c.Assert(err, IsNil)
c.Check(stat.Mode(), Equals, os.FileMode(0644))
s.RemoveSnap(c, snapInfo)
}
}

func (s *backendSuite) TestParallelInstallCombineSnippets(c *C) {
restore := apparmor_sandbox.MockLevel(apparmor_sandbox.Full)
defer restore()
Expand Down Expand Up @@ -1360,6 +1394,40 @@ func (s *backendSuite) TestSnapConfineFromSnapProfileCreatesAllDirs(c *C) {
c.Assert(osutil.IsDirectory(dirs.SnapAppArmorDir), Equals, true)
}

func (s *backendSuite) TestSnapConfineProfileIncludesEtcTunables(c *C) {
// Mock vendored apparmor
r := apparmor.MockParserFeatures(func() ([]string, error) { return []string{"snapd-internal"}, nil })
defer r()

// Let's say we're working with the core snap at revision 111.
coreInfo := snaptest.MockInfo(c, coreYaml, &snap.SideInfo{Revision: snap.R(111)})
s.writeVanillaSnapConfineProfile(c, coreInfo)

// Expect #include if exists "/etc/apparmor.d/tunables/home.d/" to be added
// right below #include <tunables/global>
expectedProfileName := "snap-confine.core.111"
expectedProfileText := fmt.Sprintf(`#include <tunables/global>
#include if exists "/etc/apparmor.d/tunables/home.d/"
%s/usr/lib/snapd/snap-confine (attach_disconnected) {
#include "%s/var/lib/snapd/apparmor/snap-confine"
# We run privileged, so be fanatical about what we include and don't use
# any abstractions
/etc/ld.so.cache r,
}
`, coreInfo.MountDir(), dirs.GlobalRootDir)

// Compute the profile and see if it matches.
_, _, content, err := apparmor.SnapConfineFromSnapProfile(coreInfo)
c.Assert(err, IsNil)
c.Assert(content, DeepEquals, map[string]osutil.FileState{
expectedProfileName: &osutil.MemoryFileState{
Content: []byte(expectedProfileText),
Mode: 0644,
},
})
}

func (s *backendSuite) TestSetupHostSnapConfineApparmorForReexecCleans(c *C) {
restorer := release.MockOnClassic(true)
defer restorer()
Expand Down
5 changes: 5 additions & 0 deletions interfaces/apparmor/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ var templateCommon = `
#include <tunables/global>
###INCLUDE_SYSTEM_TUNABLES_HOME_D_WITH_VENDORED_APPARMOR###
###INCLUDE_IF_EXISTS_SNAP_TUNING###
# snapd supports the concept of 'parallel installs' where snaps with the same
Expand Down Expand Up @@ -842,6 +843,8 @@ var coreSnippet = `
var classicTemplate = `
#include <tunables/global>
###INCLUDE_SYSTEM_TUNABLES_HOME_D_WITH_VENDORED_APPARMOR###
###VAR###
###PROFILEATTACH### (attach_disconnected,mediate_deleted) {
Expand Down Expand Up @@ -936,6 +939,8 @@ var updateNSTemplate = `
#include <tunables/global>
###INCLUDE_SYSTEM_TUNABLES_HOME_D_WITH_VENDORED_APPARMOR###
profile snap-update-ns.###SNAP_INSTANCE_NAME### (attach_disconnected) {
# The next four rules mirror those above. We want to be able to read
# and map snap-update-ns into memory but it may come from a variety of places.
Expand Down
70 changes: 70 additions & 0 deletions tests/main/snapd-homedirs-vendored/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
summary: Test that vendored apparmor does not break any system tunables

# On ubuntu 18 and below snapd-internal is not available
systems: [ubuntu-18*, ubuntu-2*]

environment:
USERNAME: home-sweet-home

prepare: |
# Create a new user in a non-standard location
mkdir -p /remote/users
useradd -b /remote/users -m -U "$USERNAME"
# Download current snapd edge
snap download --edge snapd --basename=snapd_edge
# Repack it with currently built snapd
unpackdir=/tmp/snapd-current-snap
unsquashfs -no-progress -d "${unpackdir}" snapd_edge.snap
dpkg-deb -x "${GOHOME}"/snapd_*.deb "${unpackdir}"
snap pack "${unpackdir}" --filename snapd_modified.snap
rm -rf "${unpackdir}"
restore: |
userdel -f --remove "$USERNAME"
rm -rf /remote/users
debug: |
# output custom snap-confine snippets
ls -l /var/lib/snapd/apparmor/snap-confine/
for f in /var/lib/snapd/apparmor/snap-confine/*; do
echo "$f"
cat "$f"
done
execute: |
echo "Downgrading the snapd deb to pre-vendored apparmor times"
TARGET_VER="$(apt list -a snapd|grep -- -updates|cut -f2 -d' ')"
apt install -yqq --allow-downgrades snapd="$TARGET_VER"
echo "But installing the vendored apparmor snapd with our changes"
snap install --dangerous snapd_modified.snap
# Verify supported features
snap debug sandbox-features --required apparmor:parser:snapd-internal
snap debug sandbox-features --required apparmor:parser:include-if-exists
# Install our test snap
"$TESTSTOOLS"/snaps-state install-local test-snapd-sh
echo "Invoke the test app without setting up homedir support"
if sudo -u "$USERNAME" -i test-snapd-sh.cmd echo "Hello world" 2> stderr.log; then
echo "The command succeeded; this is unexpected where AppArmor is fully working"
test "$(snap debug confinement)" = partial
else
MATCH "Sorry, home directories outside of /home needs configuration" < stderr.log
fi
rm -f stderr.log
echo "Enable the home directories under /remote/users"
snap set system homedirs=/remote/users
echo "Verify that the system-params file has been created"
MATCH "^homedirs=/remote/users$" < /var/lib/snapd/system-params
echo "And that the AppArmor tunable file is proper"
MATCH "^@{HOMEDIRS}\\+=\"/remote/users\"$" < /etc/apparmor.d/tunables/home.d/snapd
echo "Invoke the test app again (should now work)"
sudo -u "$USERNAME" -i test-snapd-sh.cmd echo "Hello world" | MATCH "Hello world"
2 changes: 2 additions & 0 deletions tests/main/snapd-homedirs-vendored/test-snapd-sh/bin/sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
exec "$@"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: test-snapd-sh
summary: A no-strings-attached, no-fuss shell for writing tests
version: 1.0

apps:
cmd:
command: bin/sh

0 comments on commit b98e4af

Please sign in to comment.