github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/interfaces/system_key.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018-2019 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package interfaces 21 22 import ( 23 "encoding/json" 24 "errors" 25 "fmt" 26 "io" 27 "io/ioutil" 28 "os" 29 "path/filepath" 30 "reflect" 31 "strconv" 32 "strings" 33 34 "github.com/snapcore/snapd/dirs" 35 "github.com/snapcore/snapd/logger" 36 "github.com/snapcore/snapd/osutil" 37 "github.com/snapcore/snapd/sandbox/apparmor" 38 "github.com/snapcore/snapd/sandbox/cgroup" 39 "github.com/snapcore/snapd/sandbox/seccomp" 40 "github.com/snapcore/snapd/snapdtool" 41 ) 42 43 // ErrSystemKeyIncomparableVersions indicates that the system-key 44 // on disk and the system-key calculated from generateSystemKey 45 // have different inputs and are therefore incomparable. 46 // 47 // This means: 48 // - "snapd" needs to re-generate security profiles 49 // - "snap run" cannot wait for those security profiles 50 var ( 51 ErrSystemKeyVersion = errors.New("system-key versions not comparable") 52 ErrSystemKeyMissing = errors.New("system-key missing on disk") 53 ) 54 55 // systemKey describes the environment for which security profiles 56 // have been generated. It is useful to compare if the current 57 // running system is similar enough to the generated profiles or 58 // if the profiles need to be re-generated to match the new system. 59 // 60 // Note that this key gets generated on *each* `snap run` - so it 61 // *must* be cheap to calculate it (no hashes of big binaries etc). 62 type systemKey struct { 63 // IMPORTANT: when adding/removing/changing inputs bump this version (see below) 64 Version int `json:"version"` 65 66 // This is the build-id of the snapd that generated the profiles. 67 BuildID string `json:"build-id"` 68 69 // These inputs come from the host environment via e.g. 70 // kernel version or similar settings. If those change we may 71 // need to change the generated profiles (e.g. when the user 72 // boots into a more featureful seccomp). 73 AppArmorFeatures []string `json:"apparmor-features"` 74 AppArmorParserMtime int64 `json:"apparmor-parser-mtime"` 75 AppArmorParserFeatures []string `json:"apparmor-parser-features"` 76 NFSHome bool `json:"nfs-home"` 77 OverlayRoot string `json:"overlay-root"` 78 SecCompActions []string `json:"seccomp-features"` 79 SeccompCompilerVersion string `json:"seccomp-compiler-version"` 80 CgroupVersion string `json:"cgroup-version"` 81 } 82 83 // IMPORTANT: when adding/removing/changing inputs bump this 84 const systemKeyVersion = 10 85 86 var ( 87 isHomeUsingNFS = osutil.IsHomeUsingNFS 88 isRootWritableOverlay = osutil.IsRootWritableOverlay 89 mockedSystemKey *systemKey 90 91 readBuildID = osutil.ReadBuildID 92 ) 93 94 func seccompCompilerVersionInfo(path string) (seccomp.VersionInfo, error) { 95 return seccomp.CompilerVersionInfo(func(name string) (string, error) { return filepath.Join(path, name), nil }) 96 } 97 98 func generateSystemKey() (*systemKey, error) { 99 // for testing only 100 if mockedSystemKey != nil { 101 return mockedSystemKey, nil 102 } 103 104 sk := &systemKey{ 105 Version: systemKeyVersion, 106 } 107 snapdPath, err := snapdtool.InternalToolPath("snapd") 108 if err != nil { 109 return nil, err 110 } 111 buildID, err := readBuildID(snapdPath) 112 if err != nil && !os.IsNotExist(err) { 113 return nil, err 114 } 115 sk.BuildID = buildID 116 117 // Add apparmor-features (which is already sorted) 118 sk.AppArmorFeatures, _ = apparmor.KernelFeatures() 119 120 // Add apparmor-parser-mtime 121 sk.AppArmorParserMtime = apparmor.ParserMtime() 122 123 // Add if home is using NFS, if so we need to have a different 124 // security profile and if this changes we need to change our 125 // profile. 126 sk.NFSHome, err = isHomeUsingNFS() 127 if err != nil { 128 // just log the error here 129 logger.Noticef("cannot determine nfs usage in generateSystemKey: %v", err) 130 return nil, err 131 } 132 133 // Add if '/' is on overlayfs so we can add AppArmor rules for 134 // upperdir such that if this changes, we change our profile. 135 sk.OverlayRoot, err = isRootWritableOverlay() 136 if err != nil { 137 // just log the error here 138 logger.Noticef("cannot determine root filesystem on overlay in generateSystemKey: %v", err) 139 return nil, err 140 } 141 142 // Add seccomp-features 143 sk.SecCompActions = seccomp.Actions() 144 145 versionInfo, err := seccompCompilerVersionInfo(filepath.Dir(snapdPath)) 146 if err != nil { 147 logger.Noticef("cannot determine seccomp compiler version in generateSystemKey: %v", err) 148 return nil, err 149 } 150 sk.SeccompCompilerVersion = string(versionInfo) 151 152 cgv, err := cgroup.Version() 153 if err != nil { 154 logger.Noticef("cannot determine cgroup version: %v", err) 155 return nil, err 156 } 157 sk.CgroupVersion = strconv.FormatInt(int64(cgv), 10) 158 159 return sk, nil 160 } 161 162 // UnmarshalJSONSystemKey unmarshalls the data from the reader as JSON into a 163 // system key usable with SystemKeysMatch. 164 func UnmarshalJSONSystemKey(r io.Reader) (interface{}, error) { 165 sk := &systemKey{} 166 err := json.NewDecoder(r).Decode(sk) 167 if err != nil { 168 return nil, err 169 } 170 return sk, nil 171 } 172 173 // WriteSystemKey will write the current system-key to disk 174 func WriteSystemKey() error { 175 sk, err := generateSystemKey() 176 if err != nil { 177 return err 178 } 179 180 // only fix AppArmorParserFeatures if we didn't already mock a system-key 181 // if we mocked a system-key we are running a test and don't want to use 182 // the real host system's parser features 183 if mockedSystemKey == nil { 184 // We only want to calculate this when the mtime of the parser changes. 185 // Since we calculate the mtime() as part of generateSystemKey, we can 186 // simply unconditionally write this out here. 187 sk.AppArmorParserFeatures, _ = apparmor.ParserFeatures() 188 } 189 190 sks, err := json.Marshal(sk) 191 if err != nil { 192 return err 193 } 194 return osutil.AtomicWriteFile(dirs.SnapSystemKeyFile, sks, 0644, 0) 195 } 196 197 // SystemKeyMismatch checks if the running binary expects a different 198 // system-key than what is on disk. 199 // 200 // This is used in two places: 201 // - snap run: when there is a mismatch it will wait for snapd 202 // to re-generate the security profiles 203 // - snapd: on startup it checks if the system-key has changed and 204 // if so re-generate the security profiles 205 // 206 // This ensures that "snap run" and "snapd" have a consistent set 207 // of security profiles. Without it we may have the following 208 // scenario: 209 // 1. snapd gets refreshed and snaps need updated security profiles 210 // to work (e.g. because snap-exec needs a new permission) 211 // 2. The system reboots to start the new snapd. At this point 212 // the old security profiles are on disk (because the new 213 // snapd did not run yet) 214 // 3. Snaps that run as daemon get started during boot by systemd 215 // (e.g. network-manager). This may happen before snapd had a 216 // chance to refresh the security profiles. 217 // 4. Because the security profiles are for the old version of 218 // the snaps that run before snapd fail to start. For e.g. 219 // network-manager this is of course catastrophic. 220 // To prevent this, in step(4) we have this wait-for-snapd 221 // step to ensure the expected profiles are on disk. 222 // 223 // The apparmor-parser-features system-key is handled specially 224 // and not included in this comparison because it is written out 225 // to disk whenever apparmor-parser-mtime changes (in this manner 226 // snap run only has to obtain the mtime of apparmor_parser and 227 // doesn't have to invoke it) 228 func SystemKeyMismatch() (bool, error) { 229 mySystemKey, err := generateSystemKey() 230 if err != nil { 231 return false, err 232 } 233 234 diskSystemKey, err := readSystemKey() 235 if err != nil { 236 return false, err 237 } 238 239 // deal with the race that "snap run" may start, then snapd 240 // is upgraded and generates a new system-key with different 241 // inputs than the "snap run" in memory. In this case we 242 // should be fine because new security profiles will also 243 // have been written to disk. 244 if mySystemKey.Version != diskSystemKey.Version { 245 return false, ErrSystemKeyVersion 246 } 247 248 // special case to detect local runs 249 if mockedSystemKey == nil { 250 if exe, err := os.Readlink("/proc/self/exe"); err == nil { 251 // detect running local local builds 252 if !strings.HasPrefix(exe, "/usr") && !strings.HasPrefix(exe, "/snap") { 253 logger.Noticef("running from non-installed location %s: ignoring system-key", exe) 254 return false, ErrSystemKeyVersion 255 } 256 } 257 } 258 259 // since we always write out apparmor-parser-feature when 260 // apparmor-parser-mtime changes, we don't need to compare it here 261 // (allowing snap run to only need to check the mtime of the parser) 262 // so just set both to nil to make the DeepEqual happy 263 diskSystemKey.AppArmorParserFeatures = nil 264 mySystemKey.AppArmorParserFeatures = nil 265 266 ok, err := SystemKeysMatch(mySystemKey, diskSystemKey) 267 return !ok, err 268 } 269 270 func readSystemKey() (*systemKey, error) { 271 raw, err := ioutil.ReadFile(dirs.SnapSystemKeyFile) 272 if err != nil && os.IsNotExist(err) { 273 return nil, ErrSystemKeyMissing 274 } 275 if err != nil { 276 return nil, err 277 } 278 var diskSystemKey systemKey 279 if err := json.Unmarshal(raw, &diskSystemKey); err != nil { 280 return nil, err 281 } 282 return &diskSystemKey, nil 283 } 284 285 // RecordedSystemKey returns the system key read from the disk as opaque interface{}. 286 func RecordedSystemKey() (interface{}, error) { 287 diskSystemKey, err := readSystemKey() 288 if err != nil { 289 return nil, err 290 } 291 return diskSystemKey, nil 292 } 293 294 // CurrentSystemKey calculates and returns the current system key as opaque interface{}. 295 func CurrentSystemKey() (interface{}, error) { 296 currentSystemKey, err := generateSystemKey() 297 return currentSystemKey, err 298 } 299 300 // SystemKeysMatch returns whether the given system keys match. 301 func SystemKeysMatch(systemKey1, systemKey2 interface{}) (bool, error) { 302 // sanity check 303 _, ok1 := systemKey1.(*systemKey) 304 _, ok2 := systemKey2.(*systemKey) 305 if !(ok1 && ok2) { 306 return false, fmt.Errorf("SystemKeysMatch: arguments are not system keys") 307 } 308 309 // TODO: write custom struct compare 310 return reflect.DeepEqual(systemKey1, systemKey2), nil 311 } 312 313 func MockSystemKey(s string) func() { 314 var sk systemKey 315 err := json.Unmarshal([]byte(s), &sk) 316 if err != nil { 317 panic(err) 318 } 319 mockedSystemKey = &sk 320 return func() { mockedSystemKey = nil } 321 }