snap: use snap-confine from the core snap #2810

Closed
wants to merge 13 commits into
from

Conversation

Projects
None yet
6 participants
Collaborator

mvo5 commented Feb 8, 2017

Not quite ready yet, tests and spread tests are missing but this allows early comments.

Missing:

  • detect and NOP on distros without apparmor
  • support debian (later?)

mvo5 added some commits Feb 7, 2017

When not OnClassic, there is no problem, right? You must reboot if the os snap is refreshed and that gives you everything. Prior to reboot, all the old stuff is still used, so no problem.

cmd/snap/cmd_run.go
+
+ cmd := []string{}
+ if release.OnClassic && osutil.FileExists(snapConfinePathInCore) {
+ cmd = append(cmd, snapConfinePathInCore)
@jdstrand

jdstrand Feb 8, 2017

Contributor

Unconditionally using snapConfinePathInCore seems problematic. Do we unconditionally use everything else from core there? What about reverts?

@mvo5

mvo5 Feb 8, 2017

Collaborator

Good point, we will have to use the same version compare magic.

overlord/snapstate/snapmgr.go
+// profile for snap-confine when we are in re-exec mode
+func (m *SnapManager) ensureSnapConfineApparmor() error {
+ if !release.OnClassic {
+ return nil
@jdstrand

jdstrand Feb 8, 2017

Contributor

Adding a comment above !release.OnClassic I think is wise. Eg:
// On all-snaps we always use the mounted snapd, snap-confine, apparmor, etc
// but on classic we use the snapd and snap-confine from the latest core snap.
// As such, nothing to do when not OnClassic

@mvo5

mvo5 Feb 8, 2017

Collaborator

Thanks, added.

overlord/snapstate/snapmgr.go
+ return nil
+ }
+
+ // FIXME2: cleanup old entries here
@jdstrand

jdstrand Feb 8, 2017

Contributor

You could cleanup only old profiles where the corresponding core snap is no longer on the system. Leaving the old ones might help the revert scenario.

On the other hand, the code would likely be simpler if instead only ever have the current core snap profile and remove all the others (that happens to mirror what we do with /var/lib/snapd/apparmor/profiles where we only have the profile for the current revision).

@mvo5

mvo5 Feb 8, 2017

Collaborator

Yes, this is done now, the simple version.

overlord/snapstate/snapmgr.go
+ // FIXME2: cleanup old entries here
+
+ // FIXME: what to do for debian? debian has a less powerful apparmor
+ // so we can not use the ubuntu profile?
@jdstrand

jdstrand Feb 8, 2017

Contributor

You can use the Ubuntu profile, you just need to add two rules:

# Required when using unpatched upstream kernel
capability sys_ptrace,
# Debian compiles snap-confine without AppArmor, so allow running
# snaps unconfined
/usr/lib/snapd/snap-exec uxr,
@mvo5

mvo5 Feb 8, 2017

Collaborator

Hm, that ties in with the suggestion from @zyga to have something more clever here than the dumb strings.Replace()

overlord/snapstate/snapmgr.go
+
+ if err := ioutil.WriteFile(apparmorProfilePath, []byte(apparmorProfileForCore), 0644); err != nil {
+ return err
+ }
@jdstrand

jdstrand Feb 8, 2017

Contributor

Please add a comment:
// /etc/apparmor.d is read/write OnClassic, so write out the new core's profile there

@mvo5

mvo5 Feb 8, 2017

Collaborator

Thanks, added.

overlord/snapstate/snapmgr.go
+
+ if output, err := exec.Command("apparmor_parser", "--replace", "--write-cache", apparmorProfilePath).CombinedOutput(); err != nil {
+ return osutil.OutputErr(output, err)
+ }
@jdstrand

jdstrand Feb 8, 2017

Contributor

This may break with alternate cores that don't have apparmor enabled. I forget OTOH how the parser handles that-- you probably want to double check that.

@mvo5

mvo5 Feb 8, 2017

Collaborator

Hm that is a good point! Should we use if release.ReleaseInfo.ForceDevMode() == true {retur nil} on top of this function?

Some comments, I think the direction is good but there's a lot of things that we need to fix before this can land.

cmd/snap/cmd_run.go
@@ -175,9 +177,17 @@ func runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string,
logger.Noticef("WARNING: cannot create user data directory: %s", err)
}
- cmd := []string{
- filepath.Join(dirs.LibExecDir, "snap-confine"),
+ // use snap-confine from core if it is available
@zyga

zyga Feb 8, 2017

Contributor

We also need to use the dynamic linker from core as snap-confine dynamically links to libraries that are not available on the host (e.g. xfs, apparmor).

@mvo5

mvo5 Feb 8, 2017

Collaborator

I added a FIXME, we will need #2791 for this first.

overlord/snapstate/snapmgr.go
@@ -551,6 +556,45 @@ func (m *SnapManager) ensureUbuntuCoreTransition() error {
return nil
}
+// ensureSnapConfineApparmor ensures that we have a valid apparmor
+// profile for snap-confine when we are in re-exec mode
+func (m *SnapManager) ensureSnapConfineApparmor() error {
@zyga

zyga Feb 8, 2017

Contributor

Question: what will keep us from keeping unbound number of old profiles around?

Perhaps we could generate the profile in /run and load it from there (the actual path is irrelevant as long as it is loaded into the kernel). This doesn't prevent unbound in-memory profiles but at least a "reboot" will fix that.

@mvo5

mvo5 Feb 8, 2017

Collaborator

This is fixed now, as @jdstrand suggested the code will only keep the "current" profile around and loaded and will discard older profiles.

overlord/snapstate/snapmgr.go
+
+ // FIXME: what to do for debian? debian has a less powerful apparmor
+ // so we can not use the ubuntu profile?
+ apparmorProfile, err := ioutil.ReadFile(filepath.Join(root, "/etc/apparmor.d/usr.lib.snapd.snap-confine"))
@zyga

zyga Feb 8, 2017

Contributor

As mentioned on telegram earlier, I think that we should ship the snap-confine.apparmor.in file and run sed on it to obtain the correct profile. We could also extend this mechanism to feed in os-release (or even apparmor version available at runtime) to generate a profile that will work in a particular environment. Using the .in file is easier for this than the resulting file from the package.

@mvo5

mvo5 Feb 8, 2017

Collaborator

I would prefer to do that in a follow-up branch, mostly because AIUI we currently ship a working /etc/apparmor.d/usr.lib.snapd.snap-confine only on Ubuntu. We also ship it on Debian but AIUI its broken there. And the other distros do not use it (yet). So it seems ideal to do a followup each time we add this file for a new distro (unless my premise is wrong of course).

overlord/snapstate/snapmgr.go
+ return err
+ }
+
+ if output, err := exec.Command("apparmor_parser", "--replace", "--write-cache", apparmorProfilePath).CombinedOutput(); err != nil {
@zyga

zyga Feb 8, 2017

Contributor

I think you should switch to interfaces.apparmor here. There are some extra option that we pass, most notably -O no-expr-simplify.

@mvo5

mvo5 Feb 8, 2017

Collaborator

The difference is the cache dir that is used. For the system profiles its /etc/apparmor.d/cache and for the snapd profiles its /var/cache/apparmor/. The /etc/init.d/apparmor init script uses the different cachedir.

@mvo5

mvo5 Feb 8, 2017

Collaborator

Having said that, of course we can refactor to something like interfaces.apparmor.AddProfileWithCachedir() or somesuch.

@jdstrand

jdstrand Feb 8, 2017

Contributor

Those options are less interesting for a single profile like snap-confine where all this happens as part of the snap refresh. -O no-expr-simplify is important on ARM with a lot of profiles that use our template, etc, but the gain will not be significant for this one profile. IMHO, this can be fixed up in a future commit if it keeps this PR simple.

overlord/snapstate/snapmgr.go
@@ -560,6 +604,12 @@ func (m *SnapManager) Ensure() error {
m.runner.Ensure()
+ // needs to run after the runner
@zyga

zyga Feb 8, 2017

Contributor

Keep in mind that snap-confine can be invoked before snapd boots. Could we perhaps trigger this from snap-run (on demand)?

We could do the same for all the "stale" objects:

  • security profiles for snaps
  • launchers (well, only if they run snap-run today)
  • snap-confine's own profile

If we detect that something is not up-to-date we reach out to snapd and call some API like ensure-ephemeral-state. Just an idea but I think we could use it to avoid costly stuff from happening on boot.

@mvo5

mvo5 Feb 8, 2017

Collaborator

This should be fine (unless I miss something). The generated apparmor profile is loaded during early boot automatically via /etc/init.d/apparmor. This ensures its available early on and its not more costly than any other of the profiles in that dir.

@jdstrand

jdstrand Feb 8, 2017

Contributor

I agree with @mvo5 that this shouldn't be needed due to how apparmor is setup during boot. @zyga, are we missing something?

@zyga

zyga Feb 13, 2017

Contributor

No I think this is fine

Contributor

jdstrand commented Feb 8, 2017

Also as a potential consideration that does not have to be fixed for this PR, /etc/apparmor.d/abstractions on the classic host and in the core snap could be different. We've been lucky so far that they haven't been different, but it is easy to imagine scenarios where they might be with different core snaps or we build the series 16 core with a different apparmor than is in xenial-updates.

mvo5 added some commits Feb 8, 2017

+
+ echo "Copy the suid root snap-confine over so that it runs outside "
+ echo "of the auto-genreated profile"
+ cp -a /snap/${core_name}/current/usr/lib/snapd/snap-confine .
@zyga

zyga Feb 9, 2017

Contributor

Please remove the copied snap-confine in restore: |

@mvo5

mvo5 Feb 10, 2017

Collaborator

Thanks! Nice catch.

zyga approved these changes Feb 13, 2017

LGTM and iterate on what needs to be done. We have more trouble without this than with the imperfections this introduces.

@mvo5 mvo5 added this to the 2.23 milestone Feb 14, 2017

overlord/snapstate/snapmgr.go
+ // not using apparmor.UnloadProfile() because it uses a
+ // different cachedir
+ if output, err := exec.Command("apparmor_parser", "-R", filepath.Base(path)).CombinedOutput(); err != nil {
+ logger.Noticef("cannot remove apparmor %s: %q", path, output)
@niemeyer

niemeyer Feb 14, 2017

Contributor

"cannot unload apparmor profile %s: %v", filepath.Base(path), osutil.OutputErr(output, err)

@mvo5

mvo5 Feb 15, 2017

Collaborator

Thanks, this is updated now

overlord/snapstate/snapmgr.go
+ // distros with apparmor around snap-confine, i.e.
+ // ship `snap-confine.apparmor.in` and use that as the base
+ // because core is ubuntu and classic host might be anything
+ apparmorProfile, err := ioutil.ReadFile(filepath.Join(root, "/etc/apparmor.d/usr.lib.snapd.snap-confine"))
@niemeyer

niemeyer Feb 14, 2017

Contributor

This is a location in classic, which means it depends on the packaging, right? At the top of this PR we have a snapConfinePath variable being used which assumes this location is not hardcoded.

Same concern below and perhaps elsewhere. Need to watch out for the difference between a path in core, which we're sure about, and in the outer system, which we're not.

@mvo5

mvo5 Feb 15, 2017

Collaborator

The apparmor profile we read as input is a location in the core snap, the "root" variable name is probably misleading here, I changed it to coreRoot to make this clearer.

overlord/snapstate/snapmgr.go
+
+ // /etc/apparmor.d is read/write OnClassic, so write out the
+ // new core's profile there
+ if err := ioutil.WriteFile(apparmorProfilePath, []byte(apparmorProfileForCore), 0644); err != nil {
@niemeyer

niemeyer Feb 14, 2017

Contributor

Shouldn't that be atomic?

@mvo5

mvo5 Feb 15, 2017

Collaborator

Indeed, I fixed it now.

overlord/snapstate/snapmgr.go
+
+ // not using apparmor.LoadProfile() because it uses a different cachedir
+ if output, err := exec.Command("apparmor_parser", "--replace", "--write-cache", apparmorProfilePath, "--cache-loc", dirs.SystemApparmorCacheDir).CombinedOutput(); err != nil {
+ return osutil.OutputErr(output, err)
@niemeyer

niemeyer Feb 14, 2017

Contributor

return fmt.Errorf("cannot replace snap-confine apparmor profile: %v", osutil.OutputErr(output, err))

@mvo5

mvo5 Feb 15, 2017

Collaborator

Thanks! Updated.

overlord/snapstate/snapmgr.go
+
+ root := filepath.Join(dirs.SnapMountDir, "/core/current/")
+ snapConfineInCore, err := filepath.EvalSymlinks(filepath.Join(root, "/usr/lib/snapd/snap-confine"))
+ // in flight
@niemeyer

niemeyer Feb 14, 2017

Contributor

Or maybe still on ubuntu-core? Or updating? Maybe there's no core at all at this point? Comment seems to not help much. If we know exactly why this should happen here, then we need to be clear why for our own benefit later. Otherwise, I suggest just dropping it so we're not creating a false impression.

@mvo5

mvo5 Feb 15, 2017

Collaborator

I improved the comment, I hope it makes it clearer.

overlord/snapstate/snapmgr.go
@@ -560,6 +660,12 @@ func (m *SnapManager) Ensure() error {
m.runner.Ensure()
+ // needs to run after the runner
@niemeyer

niemeyer Feb 14, 2017

Contributor

Why it needs to run after the runner? Otherwise it's a riddle for the next reader.

@mvo5

mvo5 Feb 15, 2017

Collaborator

Sorry for this! Comment updated and it is hopefully more descriptive now :)

overlord/snapstate/snapmgr.go
@@ -560,6 +660,12 @@ func (m *SnapManager) Ensure() error {
m.runner.Ensure()
+ // needs to run after the runner
+ if err := m.ensureSnapConfineApparmor(); err != nil {
+ // FIMXE: this will spam the world as ensure runs often
@niemeyer

niemeyer Feb 14, 2017

Contributor

Why are we doing that non-stop? I mean reading/writing/updating the profile, rather than just logging. That sounds unnecessary and somewhat unfortunate to be running so frequently. Shouldn't we be doing that only when updating core, which is the moment when we update snap-confine itself and the apparmor profile that comes with it?

@mvo5

mvo5 Feb 15, 2017

Collaborator

We have some options here, I'm happy to rework it. The implementation is done like this because:
a) on startup we need to check if we have an apparmor profile for snap-confine. we may upgrade/re-exec from an older version of snapd so we need to check this, the ensure loop seems to be a good place for this
b) if we need to have this code anyway it can be reused for the case when the core updates. please note that it will not actually re-generate profiles all the time. if the profile exists, it will just do nothing.

We can of course change that to:
a) check once on startup if the profile is there
b) write the snap-confine profile in e.g. the setup-security task of core (as a special case there).

Happy to rework if you think the later design is cleaner (or anyother design that is cleaner!).

overlord/snapstate/snapmgr.go
+ // needs to run after the runner
+ if err := m.ensureSnapConfineApparmor(); err != nil {
+ // FIMXE: this will spam the world as ensure runs often
+ logger.Noticef("ensureSnapConfineApparmor failed with: %s", err)
@niemeyer

niemeyer Feb 14, 2017

Contributor

("cannot update apparmor profile from core snap: %v", err)

@mvo5

mvo5 Feb 15, 2017

Collaborator

Thanks, fixed.

+ // apparmor, etc but on classic we use the snapd and
+ // snap-confine from the latest core snap. As such, nothing
+ // to do when not OnClassic
+ if !release.OnClassic {
@niemeyer

niemeyer Feb 14, 2017

Contributor

One more detail: where are we deciding to not do this logic at all because the distribution at hand doesn't support apparmor?

@mvo5

mvo5 Feb 15, 2017

Collaborator

We don't have that right now, I fixed it and use if release.ReleaseInfo.ForceDevMode() { return nil } now.

mvo5 added some commits Feb 14, 2017

zyga approved these changes Feb 15, 2017

Nice

+ }
+ // On releases that do not support apparmor there is no need to
+ // write an apparmor profile for snap-confine
+ if release.ReleaseInfo.ForceDevMode() {
@zyga

zyga Feb 15, 2017

Contributor

This is not super-strictly-speaking true. Can you please add a comment like this:

// TODO: switch to runtime-detected flag that tells us if apparmor is enabled.
// Write a profile for snap-confine unconditionally but only attempt to load it
// if apparmor is enabled.

@mvo5 mvo5 modified the milestones: 2.24, 2.23 Feb 16, 2017

pedronis requested changes Feb 17, 2017 edited

yes, where/when and how to run apparmor profile regen is a bit unclear to me atm

@@ -560,6 +666,13 @@ func (m *SnapManager) Ensure() error {
m.runner.Ensure()
+ // run after the runner so if we get a new core snap, we immediately
+ // generate the matching apparmor profile for snap-confine from core
@pedronis

pedronis Feb 17, 2017

Contributor

indeed I don't understand this comment, also I don't see any kind of locks ? it feels also strange to do all this in snapmgr and not ifacemgr

Collaborator

mvo5 commented Feb 23, 2017

After discussing the security profile generation for the snap.core.$revno.usr.lib.snapd.snap-confine part, there will be some changes needed:

  • generate the security as part of the setup-security task of core (and remove the code in ensure)
  • fix the bug that a new core snap does not refresh the security profiles (probably a separate branch)
    This will ensure that the profile is available for snap-confine to work from the core snap.
Collaborator

mvo5 commented Feb 23, 2017

Closing for now as this needs some rework (see last comment)

@mvo5 mvo5 closed this Feb 23, 2017

Contributor

jdstrand commented Mar 22, 2017

This is blocking a bunch of security team work regarding seccomp argument filtering policy as well a undoing 21bc6b9 which contains a security fix. When can work on this PR be resumed? Can we target this for 2.24 so at a minimum 21bc6b9 can be reverted?

- if su test -c "sh -c \"SNAP_NAME=test-snapd-tools /snap/${core_name}/current/usr/lib/snapd/snap-confine snap.test-snapd-tools.cmd /bin/true 2>/dev/null\""; then
+
+ echo "Copy the suid root snap-confine over so that it runs outside "
+ echo "of the auto-genreated profile"
@setharnold

setharnold Mar 22, 2017

Typo "genreated" in what appears to be user-facing string. Thanks.

Contributor

jdstrand commented Mar 29, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment