Files
snapd/interfaces/system_key.go
Oliver Calder 0ff642e82e many: include prompt prefix in apparmor rules (#13822)
* features,i/{apparmor,builtin}: include prompt prefix in home interface

If prompting is supported and enabled, include the prompt prefix in
AppArmor rules for the home interface, which will cause AppArmor to send
a prompt when accessing any file in $HOME.

In the future, if other interfaces include the ###PROMPT### prefix in
their rule snippets, this will also be handled accordingly.

At the moment, the status of prompting support is checked whenever the
AppArmor backend prepares profiles. This is okay, since AppArmor support
for prompting depends on kernel and parser features, which are only
probed once after snapd starts. However, to ensure that the same
supported value is used even if that were not the case, and in case we
wish to only use the prompt prefix for some snaps or interfaces, we may
wish to embed whether to use the prompt prefix in the AppArmor
Specification instead.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* features: adjust unsupported messages when checking apparmor features errors

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* interfaces: add prompting status to system key

Include whether prompting is supported and enabled as a single field in
the system key. This way, if `(supported && enabled)` changes, security
profiles will be regenerated when snapd starts up.

Currently, prompting support only changes when the AppArmor kernel or
parser features change, and profile regeneration is the only other place
where it is checked whether AppArmor prompting is supported and enabled.
Thus, including whether prompting is supported and enabled in the system
key ensures that security profiles are regenerated when necessary during
snapd startup, and only when necessary (e.g. not if support changed but
prompting flag remained disabled nor if flag changed but prompting
remained unsupported).

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* o/c/configcore: restart snapd when prompting value changes

When the prompting flag changes and the result entails that security
profiles should be regenerated, restart snapd to do so.

This is required iff prompting is supported and the experimental
apparmor-prompting flag changes -- if prompting is not supported,
prompting can't be used, so no need to regenerate profiles. Importantly,
prompting support is based entirely on the available AppArmor kernel and
parser features, and these are only probed once during snapd startup, so
prompting support cannot change (under the current implementation)
except when snapd restarts.

Since `(supported && enabled)` is part of the system key, and a restart
is only triggered if prompting is supported and the flag value changes
(which is equivalent to `(supported && enabled)`, since the supported
value cannot change while snapd is running), restarting after the flag
has changed causes the system key to be different, and thus to trigger a
security profile regeneration, as desired.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* interfaces,o/ifacestate: set prompting in apparmor spec

Rather than checking whether AppArmor prompting is supported and enabled
whenever the AppArmor backend is processing a snippet, instead include
that precomputed value in the Specification itself, and place it there
via `buildConfinementOptions`. This way, any spec created with the same
`confinementOptions` will make the same decision as to whether to
include prompt prefixes on relevant rules.

Currently, `buildConfinementOptions` simply checks whether prompting is
supported and enabled via the methods on `features.AppArmorPrompting`,
but ideally, this value would be looked up from either the system key
or by checking whether the prompting listener is running. It remains to
be seen how the value computed as part of the system key can be
guaranteed to be the same as that used elsewhere, either in
`buildConfinementOptions` or when deciding whether to start the
listener.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* o/c/configcore: request snapd restart whenever prompting flag changes

Previously, a snapd restart was only requested when the status of the
"apparmor-prompting" experimental feature flag changed and prompting was
supported. However, since prompting support is dependent on AppArmor
kernel and parser features which are probed only once during startup,
and systems which do not use vendored AppArmor may have had an update to
the system AppArmor package which newly supports AppArmor prompting, it
is safer to request a restart of snapd to re-check for prompting
support.

This way, if one is enabling prompting for the first time on a system
without prompting support, they can have snapd installed, update their
kernel or apparmor installation to support prompting, and then set the
prompting flag to enable prompting without needing to manually restart
snapd.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* interfaces: support optional trailing space after ###PROMPT###

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

i/apparmor: move promptReplacer definition to before its use

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: add test of restart behavior when setting experimental.apparmor-prompting

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: refactor prompting test to reset failed count and safely check for restarts

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

tests: add shellcheck exception for apparmor prompting flag restart test

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: check that snapd PID != 0 and use snap changes to wait for feature change to complete

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: check for start-limit-hit before calling reset-failed

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: add ubuntu core to apparmor prompting flag restart test

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: check apparmor-prompting value after setting it unchanged

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* Revert "tests: check for start-limit-hit before calling reset-failed"

This reverts commit bea68516c3287fa44d6718f0794484746ae99ac5.

* tests: check systemd start-limit-hit when apparmor-prompting flag changed

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* o/configstate/configcore: add unit tests for doExperimentalApparmorPromptingDaemonRestart

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/builtin: add missing prompt prefix and adjust test

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* i/apparmor: add test for prompt prefix substitution

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* many: pass prompting value into system key functions

As such, we now precompute whether prompting is supported and enabled in
`InterfaceManager.StartUp()`, store it in the `InterfaceManager`
instance, and pass it into the call to `WriteSystemKey()`.

Additionally, we make `buildConfinementOptions` a method of
`InterfaceManager`, thus eliminating the need to check within the system
key functions whether prompting is supported and enabled.

However, there remains a problem that `snap run` calls
`SystemKeyMismatch()`, which previously invoked
`apparmor.ParserFeatures()` via `AppArmorPrompting.IsSupported()`, and
now calls `AppArmorPrompting.IsSupported()` directly and passes the
result into `SystemKeyMismatch()`. In either case, we really want this
to be avoided if at all possible, since `snap run` does not have access
to the cached value from the `InterfaceManager`, and thus must invoke
the `apparmor_parser` binary to check parser features whenever we want
to run any snap.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* many: don't probe parser features when checking system key mismatch

Since `snap run` calls `SystemKeyMismatch()`, we want to avoid needing
to call `AppArmorPrompting.IsSupported()` if at all possible, since this
calls `apparmor.ParserFeatures()`, which executes the `apparmor_parser`
binary. We can and should call `AppArmorPrompting.IsSupported()` when
writing the system key, but not when checking for a mismatch.

The system key written to disk should correctly hold the list of kernel
and parser features, the parser mtime, and whether or not prompting was
previously supported and enabled. We can check whether apparmor parser
features have changed by checking the parser mtime, without needing to
probe parser features -- this optimization is already used by
`SystemKeyMismatch()`. If we knew whether prompting was previously
supported (regardless of whether it was enabled), then so long as the
parser and kernel features are unchanged, we know that prompting support
is also unchanged.

Thus, if we add a second prompting-related field to the system key, this
one storing whether prompting is supported (ignoring enabled), we can
check if prompting support is unchanged without needing to call
`AppArmorPrompting.IsSupported()`.

Furthermore, `SystemKeyMismatch()` is the function in question, and if
there is any mismatch detected, it should return such as soon as
possible, regardless of what the mismatch is. Therefore, if we know that
either kernel or parser features have changed, then we can immediately
return that there is a mismatch, and we don't need to check whether
those feature changes affect prompting support.

Therefore, the new cases which we must worry about when checking for a
system key mismatch are the following, when all other system key fields
are unchanged (note that prompting must be supported in order to be
supported&&enabled):

1. supported: F, supported&&enabled: F, newFlag: F, mismatch: F
2. supported: F, supported&&enabled: F, newFlag: T, mismatch: F
3. supported: T, supported&&enabled: F, newFlag: F, mismatch: F
4. supported: T, supported&&enabled: F, newFlag: T, mismatch: T
5. supported: T, supported&&enabled: T, newFlag: F, mismatch: T
6. supported: T, supported&&enabled: T, newFlag: T, mismatch: F

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* interfaces: fix test string formatting

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* o/configstate/configcore: adjust prompting-related comments

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: increase prompting check_snapd_restarted timeout and add systemd show

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: reset start limit when checking if snapd restarted after prompting change

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* many: add system key extra data to hold prompting enabled status

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* many: only store one apparmor prompting value in system key

When checking for a system key mismatch, use the stored AppArmor
parser features from the system key on disk (along with the kernel
features from the newly-generated key) to check whether prompting is
supported, and AND that with the `AppArmorPrompting` value passed in
with the `SystemKeyExtraData`. If the kernel or parser features have
changed, the system key will be a mismatch anyway, so it is perfectly
safe to use the existing parser features to check for prompting support.

As such, the functions to check for prompting support have been moved
from `features` to `sandbox/apparmor`, and the support check has been
separated from the call to get `ParserFeatures()` and
`KernelFeatures()`, so that the values from the system key can be passed
in instead of invoking those functions.

Using the system key's stored parser and kernel features, there is no
need to save whether prompting is supported as part of the system key,
simplifying the key and the logic used to set the prompting value.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: explicitly install jq in apparmor-prompting-flag-restart test

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* many: consolidate checks for apparmor prompting support

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* interfaces,s/apparmor: use features struct when checking prompting support

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: improve logging in apparmor-prompting-flag-restart test

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

* tests: fix prompting flag restart test on core18

For some reason, when snapd fails due to start-limit-hit on core18, the
snapd.failure.service starts and acquires the state lock, thus
preventing snapd from successfully becoming "active" again and leaving
it retrying at "activating". It is unclear why this happens on core18
and not elsewhere.

As a fix, when resetting the start limit, stop snapd.failure.service
manually to ensure that snapd can successfully start.

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>

---------

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
2024-06-11 18:13:00 +01:00

378 lines
13 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2018-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package interfaces
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/sandbox/apparmor"
"github.com/snapcore/snapd/sandbox/cgroup"
"github.com/snapcore/snapd/sandbox/seccomp"
"github.com/snapcore/snapd/snapdtool"
)
// ErrSystemKeyIncomparableVersions indicates that the system-key
// on disk and the system-key calculated from generateSystemKey
// have different inputs and are therefore incomparable.
//
// This means:
// - "snapd" needs to re-generate security profiles
// - "snap run" cannot wait for those security profiles
var (
ErrSystemKeyVersion = errors.New("system-key versions not comparable")
ErrSystemKeyMissing = errors.New("system-key missing on disk")
)
// systemKey describes the environment for which security profiles
// have been generated. It is useful to compare if the current
// running system is similar enough to the generated profiles or
// if the profiles need to be re-generated to match the new system.
//
// Note that this key gets generated on *each* `snap run` - so it
// *must* be cheap to calculate it (no hashes of big binaries etc).
type systemKey struct {
// IMPORTANT: when adding/removing/changing inputs bump this version (see below)
Version int `json:"version"`
// This is the build-id of the snapd that generated the profiles.
BuildID string `json:"build-id"`
// These inputs come from the host environment via e.g.
// kernel version or similar settings. If those change we may
// need to change the generated profiles (e.g. when the user
// boots into a more featureful seccomp).
//
// As an exception, the NFSHome is not renamed to RemoteFSHome
// to avoid needless re-computation.
AppArmorFeatures []string `json:"apparmor-features"`
AppArmorParserMtime int64 `json:"apparmor-parser-mtime"`
AppArmorParserFeatures []string `json:"apparmor-parser-features"`
AppArmorPrompting bool `json:"apparmor-prompting"`
NFSHome bool `json:"nfs-home"`
OverlayRoot string `json:"overlay-root"`
SecCompActions []string `json:"seccomp-features"`
SeccompCompilerVersion string `json:"seccomp-compiler-version"`
CgroupVersion string `json:"cgroup-version"`
}
// IMPORTANT: when adding/removing/changing inputs bump this
const systemKeyVersion = 11
var (
isHomeUsingRemoteFS = osutil.IsHomeUsingRemoteFS
isRootWritableOverlay = osutil.IsRootWritableOverlay
mockedSystemKey *systemKey
readBuildID = osutil.ReadBuildID
)
func seccompCompilerVersionInfo(path string) (seccomp.VersionInfo, error) {
return seccomp.CompilerVersionInfo(func(name string) (string, error) { return filepath.Join(path, name), nil })
}
func generateSystemKey() (*systemKey, error) {
// for testing only
if mockedSystemKey != nil {
return mockedSystemKey, nil
}
sk := &systemKey{
Version: systemKeyVersion,
}
snapdPath, err := snapdtool.InternalToolPath("snapd")
if err != nil {
return nil, err
}
buildID, err := readBuildID(snapdPath)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
sk.BuildID = buildID
// Add apparmor-features (which is already sorted)
sk.AppArmorFeatures, _ = apparmor.KernelFeatures()
// Add apparmor-parser-mtime
sk.AppArmorParserMtime = apparmor.ParserMtime()
// Add if home is using a remote file system, if so we need to have a
// different security profile and if this changes we need to change our
// profile.
sk.NFSHome, err = isHomeUsingRemoteFS()
if err != nil {
// just log the error here
logger.Noticef("cannot determine nfs usage in generateSystemKey: %v", err)
return nil, err
}
// Add if '/' is on overlayfs so we can add AppArmor rules for
// upperdir such that if this changes, we change our profile.
sk.OverlayRoot, err = isRootWritableOverlay()
if err != nil {
// just log the error here
logger.Noticef("cannot determine root filesystem on overlay in generateSystemKey: %v", err)
return nil, err
}
// Add seccomp-features
sk.SecCompActions = seccomp.Actions()
versionInfo, err := seccompCompilerVersionInfo(filepath.Dir(snapdPath))
if err != nil {
logger.Noticef("cannot determine seccomp compiler version in generateSystemKey: %v", err)
return nil, err
}
sk.SeccompCompilerVersion = string(versionInfo)
cgv, err := cgroup.Version()
if err != nil {
logger.Noticef("cannot determine cgroup version: %v", err)
return nil, err
}
sk.CgroupVersion = strconv.FormatInt(int64(cgv), 10)
return sk, nil
}
// UnmarshalJSONSystemKey unmarshalls the data from the reader as JSON into a
// system key usable with SystemKeysMatch.
func UnmarshalJSONSystemKey(r io.Reader) (interface{}, error) {
sk := &systemKey{}
err := json.NewDecoder(r).Decode(sk)
if err != nil {
return nil, err
}
return sk, nil
}
// SystemKeyExtraData holds information about the current state of the system
// key so that some values do not need to be re-checked and can thus be
// guaranteed to be consistent across multiple uses of system key functions.
type SystemKeyExtraData struct {
// AppArmorPrompting indicates whether AppArmorPrompting should be set in
// the system key, assuming that prompting is supported. If prompting is
// unsupported, the value in the system key will be set to false.
AppArmorPrompting bool
}
var apparmorPromptingSupportedByFeatures = apparmor.PromptingSupportedByFeatures
// WriteSystemKey will write the current system-key to disk
func WriteSystemKey(extraData SystemKeyExtraData) error {
sk, err := generateSystemKey()
if err != nil {
return err
}
// only fix AppArmorParserFeatures if we didn't already mock a system-key
// if we mocked a system-key we are running a test and don't want to use
// the real host system's parser features
if mockedSystemKey == nil {
// We only want to calculate this when the mtime of the parser changes.
// Since we calculate the mtime() as part of generateSystemKey, we can
// simply unconditionally write this out here.
sk.AppArmorParserFeatures, _ = apparmor.ParserFeatures()
}
// AppArmorPrompting should be true if the given extra data prompting value
// is true and if the AppArmor kernel and parser features support prompting.
apparmorFeatures := apparmor.FeaturesSupported{
KernelFeatures: sk.AppArmorFeatures,
ParserFeatures: sk.AppArmorParserFeatures,
}
promptingSupported, _ := apparmorPromptingSupportedByFeatures(&apparmorFeatures)
sk.AppArmorPrompting = extraData.AppArmorPrompting && promptingSupported
sks, err := json.Marshal(sk)
if err != nil {
return err
}
return osutil.AtomicWriteFile(dirs.SnapSystemKeyFile, sks, 0644, 0)
}
// SystemKeyMismatch checks if the running binary expects a different
// system-key than what is on disk.
//
// This is used in two places:
// - snap run: when there is a mismatch it will wait for snapd
// to re-generate the security profiles
// - snapd: on startup it checks if the system-key has changed and
// if so re-generate the security profiles
//
// This ensures that "snap run" and "snapd" have a consistent set
// of security profiles. Without it we may have the following
// scenario:
// 1. snapd gets refreshed and snaps need updated security profiles
// to work (e.g. because snap-exec needs a new permission)
// 2. The system reboots to start the new snapd. At this point
// the old security profiles are on disk (because the new
// snapd did not run yet)
// 3. Snaps that run as daemon get started during boot by systemd
// (e.g. network-manager). This may happen before snapd had a
// chance to refresh the security profiles.
// 4. Because the security profiles are for the old version of
// the snaps that run before snapd fail to start. For e.g.
// network-manager this is of course catastrophic.
//
// To prevent this, in step(4) we have this wait-for-snapd
// step to ensure the expected profiles are on disk.
//
// The apparmor-parser-features system-key is handled specially
// and not included in this comparison because it is written out
// to disk whenever apparmor-parser-mtime changes (in this manner
// snap run only has to obtain the mtime of apparmor_parser and
// doesn't have to invoke it)
func SystemKeyMismatch(extraData SystemKeyExtraData) (bool, error) {
mySystemKey, err := generateSystemKey()
if err != nil {
return false, err
}
diskSystemKey, err := readSystemKey()
if err != nil {
return false, err
}
// deal with the race that "snap run" may start, then snapd
// is upgraded and generates a new system-key with different
// inputs than the "snap run" in memory. In this case we
// should be fine because new security profiles will also
// have been written to disk.
if mySystemKey.Version != diskSystemKey.Version {
return false, ErrSystemKeyVersion
}
// special case to detect local runs
if mockedSystemKey == nil {
if exe, err := os.Readlink("/proc/self/exe"); err == nil {
// detect running local local builds
if !strings.HasPrefix(exe, "/usr") && !strings.HasPrefix(exe, "/snap") {
logger.Noticef("running from non-installed location %s: ignoring system-key", exe)
return false, ErrSystemKeyVersion
}
}
}
// Store previous parser features so we can use them later, if unchanged
parserFeatures := diskSystemKey.AppArmorParserFeatures
// since we always write out apparmor-parser-feature when
// apparmor-parser-mtime changes, we don't need to compare it here
// (allowing snap run to only need to check the mtime of the parser)
// so just set both to nil to make the DeepEqual happy
diskSystemKey.AppArmorParserFeatures = nil
mySystemKey.AppArmorParserFeatures = nil
// AppArmorPrompting should be true if the given extra data prompting value
// is true and if the AppArmor kernel and parser features support prompting.
// Since generateSystemKey() does not exec apparmor_parser to check parser
// features, we cannot use mySystemKey parser features to check prompting
// support. If parser features differ between mySystemKey and diskSystemKey,
// then parser mtime will differ and we'll return true anyway. If parser
// features are the same, then we can use the disk parser features to check
// if AppArmorPrompting should be set.
apparmorFeatures := apparmor.FeaturesSupported{
KernelFeatures: mySystemKey.AppArmorFeatures,
ParserFeatures: parserFeatures,
}
promptingSupported, _ := apparmorPromptingSupportedByFeatures(&apparmorFeatures)
mySystemKey.AppArmorPrompting = extraData.AppArmorPrompting && promptingSupported
ok, err := SystemKeysMatch(mySystemKey, diskSystemKey)
if err != nil || !ok {
return true, err
}
return false, nil
}
func readSystemKey() (*systemKey, error) {
raw, err := os.ReadFile(dirs.SnapSystemKeyFile)
if err != nil && os.IsNotExist(err) {
return nil, ErrSystemKeyMissing
}
if err != nil {
return nil, err
}
var diskSystemKey systemKey
if err := json.Unmarshal(raw, &diskSystemKey); err != nil {
return nil, err
}
return &diskSystemKey, nil
}
// RecordedSystemKey returns the system key read from the disk as opaque interface{}.
func RecordedSystemKey() (interface{}, error) {
diskSystemKey, err := readSystemKey()
if err != nil {
return nil, err
}
return diskSystemKey, nil
}
// CurrentSystemKey calculates and returns the current system key as opaque interface{}.
func CurrentSystemKey() (interface{}, error) {
currentSystemKey, err := generateSystemKey()
return currentSystemKey, err
}
// SystemKeysMatch returns whether the given system keys match.
func SystemKeysMatch(systemKey1, systemKey2 interface{}) (bool, error) {
// precondition check
_, ok1 := systemKey1.(*systemKey)
_, ok2 := systemKey2.(*systemKey)
if !(ok1 && ok2) {
return false, fmt.Errorf("SystemKeysMatch: arguments are not system keys")
}
// TODO: write custom struct compare
return reflect.DeepEqual(systemKey1, systemKey2), nil
}
// RemoveSystemKey removes the system key from the disk.
func RemoveSystemKey() error {
err := os.Remove(dirs.SnapSystemKeyFile)
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func MockSystemKey(s string) func() {
var sk systemKey
err := json.Unmarshal([]byte(s), &sk)
if err != nil {
panic(err)
}
mockedSystemKey = &sk
return func() { mockedSystemKey = nil }
}