gitee.com/mysnapcore/mysnapd@v0.1.0/interfaces/apparmor/backend.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2020 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 apparmor implements integration between snappy and 21 // ubuntu-core-launcher around apparmor. 22 // 23 // Snappy creates apparmor profiles for each application (for each snap) 24 // present in the system. Upon each execution of ubuntu-core-launcher 25 // application process is launched under the profile. Prior to that the profile 26 // must be parsed, compiled and loaded into the kernel using the support tool 27 // "apparmor_parser". 28 // 29 // Each apparmor profile contains a simple <header><content><footer> structure. 30 // The header specifies the profile name that the launcher will use to launch a 31 // process under this profile. Snappy uses "abstract identifiers" as profile 32 // names. 33 // 34 // The actual profiles are stored in /var/lib/snappy/apparmor/profiles. 35 // 36 // NOTE: A systemd job (apparmor.service) loads all snappy-specific apparmor 37 // profiles into the kernel during the boot process. 38 package apparmor 39 40 import ( 41 "bytes" 42 "fmt" 43 "io/ioutil" 44 "os" 45 "path" 46 "path/filepath" 47 "regexp" 48 "sort" 49 "strings" 50 51 "gitee.com/mysnapcore/mysnapd/dirs" 52 "gitee.com/mysnapcore/mysnapd/interfaces" 53 "gitee.com/mysnapcore/mysnapd/logger" 54 "gitee.com/mysnapcore/mysnapd/osutil" 55 "gitee.com/mysnapcore/mysnapd/release" 56 apparmor_sandbox "gitee.com/mysnapcore/mysnapd/sandbox/apparmor" 57 "gitee.com/mysnapcore/mysnapd/snap" 58 "gitee.com/mysnapcore/mysnapd/strutil" 59 "gitee.com/mysnapcore/mysnapd/timings" 60 ) 61 62 var ( 63 procSelfExe = "/proc/self/exe" 64 isHomeUsingNFS = osutil.IsHomeUsingNFS 65 isRootWritableOverlay = osutil.IsRootWritableOverlay 66 kernelFeatures = apparmor_sandbox.KernelFeatures 67 parserFeatures = apparmor_sandbox.ParserFeatures 68 loadProfiles = apparmor_sandbox.LoadProfiles 69 unloadProfiles = apparmor_sandbox.UnloadProfiles 70 71 // make sure that apparmor profile fulfills the late discarding backend 72 // interface 73 _ interfaces.SecurityBackendDiscardingLate = (*Backend)(nil) 74 ) 75 76 // Backend is responsible for maintaining apparmor profiles for snaps and parts of snapd. 77 type Backend struct { 78 preseed bool 79 80 coreSnap *snap.Info 81 snapdSnap *snap.Info 82 } 83 84 // Name returns the name of the backend. 85 func (b *Backend) Name() interfaces.SecuritySystem { 86 return interfaces.SecurityAppArmor 87 } 88 89 // Initialize prepares customized apparmor policy for snap-confine. 90 func (b *Backend) Initialize(opts *interfaces.SecurityBackendOptions) error { 91 if opts != nil && opts.Preseed { 92 b.preseed = true 93 } 94 95 if opts != nil { 96 b.coreSnap = opts.CoreSnapInfo 97 b.snapdSnap = opts.SnapdSnapInfo 98 } 99 // NOTE: It would be nice if we could also generate the profile for 100 // snap-confine executing from the core snap, right here, and not have to 101 // do this in the Setup function below. I sadly don't think this is 102 // possible because snapd must be able to install a new core and only at 103 // that moment generate it. 104 105 // Inspect the system and sets up local apparmor policy for snap-confine. 106 // Local policy is included by the system-wide policy. If the local policy 107 // has changed then the apparmor profile for snap-confine is reloaded. 108 109 // Create the local policy directory if it is not there. 110 if err := os.MkdirAll(dirs.SnapConfineAppArmorDir, 0755); err != nil { 111 return fmt.Errorf("cannot create snap-confine policy directory: %s", err) 112 } 113 114 // Check the /proc/self/exe symlink, this is needed below but we want to 115 // fail early if this fails for whatever reason. 116 exe, err := os.Readlink(procSelfExe) 117 if err != nil { 118 return fmt.Errorf("cannot read %s: %s", procSelfExe, err) 119 } 120 121 // Location of the generated policy. 122 glob := "*" 123 policy := make(map[string]osutil.FileState) 124 125 // Check if NFS is mounted at or under $HOME. Because NFS is not 126 // transparent to apparmor we must alter our profile to counter that and 127 // allow snap-confine to work. 128 if nfs, err := isHomeUsingNFS(); err != nil { 129 logger.Noticef("cannot determine if NFS is in use: %v", err) 130 } else if nfs { 131 policy["nfs-support"] = &osutil.MemoryFileState{ 132 Content: []byte(nfsSnippet), 133 Mode: 0644, 134 } 135 logger.Noticef("snapd enabled NFS support, additional implicit network permissions granted") 136 } 137 138 // Check if '/' is on overlayfs. If so, add the necessary rules for 139 // upperdir and allow snap-confine to work. 140 if overlayRoot, err := isRootWritableOverlay(); err != nil { 141 logger.Noticef("cannot determine if root filesystem on overlay: %v", err) 142 } else if overlayRoot != "" { 143 snippet := strings.Replace(overlayRootSnippet, "###UPPERDIR###", overlayRoot, -1) 144 policy["overlay-root"] = &osutil.MemoryFileState{ 145 Content: []byte(snippet), 146 Mode: 0644, 147 } 148 logger.Noticef("snapd enabled root filesystem on overlay support, additional upperdir permissions granted") 149 } 150 151 // Check whether apparmor_parser supports bpf capability. Some older 152 // versions do not, hence the capability cannot be part of the default 153 // profile of snap-confine as loading it would fail. 154 if features, err := apparmor_sandbox.ParserFeatures(); err != nil { 155 logger.Noticef("cannot determine apparmor_parser features: %v", err) 156 } else if strutil.ListContains(features, "cap-bpf") { 157 policy["cap-bpf"] = &osutil.MemoryFileState{ 158 Content: []byte(capabilityBPFSnippet), 159 Mode: 0644, 160 } 161 } 162 163 // Ensure that generated policy is what we computed above. 164 created, removed, err := osutil.EnsureDirState(dirs.SnapConfineAppArmorDir, glob, policy) 165 if err != nil { 166 return fmt.Errorf("cannot synchronize snap-confine policy: %s", err) 167 } 168 if len(created) == 0 && len(removed) == 0 { 169 // If the generated policy didn't change, we're all done. 170 return nil 171 } 172 173 // If snapd is executing from the core snap the it means it has 174 // re-executed. In that case we are no longer using the copy of 175 // snap-confine from the host distribution but our own copy. We don't have 176 // to re-compile and load the updated profile as that is performed by 177 // setupSnapConfineReexec below. 178 if strings.HasPrefix(exe, dirs.SnapMountDir) { 179 return nil 180 } 181 182 // Reload the apparmor profile of snap-confine. This points to the main 183 // file in /etc/apparmor.d/ as that file contains include statements that 184 // load any of the files placed in /var/lib/snapd/apparmor/snap-confine/. 185 profilePath := apparmor_sandbox.SnapConfineDistroProfilePath() 186 if profilePath == "" { 187 // XXX: is profile mandatory on some distros? 188 189 // There is no AppArmor profile for snap-confine, quite likely 190 // AppArmor support is enabled in the kernel and relevant 191 // userspace tools exist, but snap-confine was built without it, 192 // nothing we need to update then. 193 logger.Noticef("snap-confine apparmor profile is absent, nothing to update") 194 return nil 195 } 196 197 aaFlags := apparmor_sandbox.SkipReadCache 198 if b.preseed { 199 aaFlags |= apparmor_sandbox.SkipKernelLoad 200 } 201 202 if err := loadProfiles([]string{profilePath}, apparmor_sandbox.SystemCacheDir, aaFlags); err != nil { 203 // When we cannot reload the profile then let's remove the generated 204 // policy. Maybe we have caused the problem so it's better to let other 205 // things work. 206 osutil.EnsureDirState(dirs.SnapConfineAppArmorDir, glob, nil) 207 return fmt.Errorf("cannot reload snap-confine apparmor profile: %v", err) 208 } 209 return nil 210 } 211 212 // snapConfineFromSnapProfile returns the apparmor profile for 213 // snap-confine in the given core/snapd snap. 214 func snapConfineFromSnapProfile(info *snap.Info) (dir, glob string, content map[string]osutil.FileState, err error) { 215 // TODO: fix this for distros using /usr/libexec/snapd when those start 216 // to use reexec 217 218 // Find the vanilla apparmor profile for snap-confine as present in the given core snap. 219 220 // We must test the ".real" suffix first, this is a workaround for 221 // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=858004 222 vanillaProfilePath := filepath.Join(info.MountDir(), "/etc/apparmor.d/usr.lib.snapd.snap-confine.real") 223 vanillaProfileText, err := ioutil.ReadFile(vanillaProfilePath) 224 if os.IsNotExist(err) { 225 vanillaProfilePath = filepath.Join(info.MountDir(), "/etc/apparmor.d/usr.lib.snapd.snap-confine") 226 vanillaProfileText, err = ioutil.ReadFile(vanillaProfilePath) 227 } 228 if err != nil { 229 return "", "", nil, fmt.Errorf("cannot open apparmor profile for vanilla snap-confine: %s", err) 230 } 231 232 // Replace the path to vanilla snap-confine with the path to the mounted snap-confine from core. 233 snapConfineInCore := filepath.Join(info.MountDir(), "usr/lib/snapd/snap-confine") 234 patchedProfileText := bytes.Replace( 235 vanillaProfileText, []byte("/usr/lib/snapd/snap-confine"), []byte(snapConfineInCore), -1) 236 237 // Also replace the test providing access to verbatim 238 // /usr/lib/snapd/snap-confine, which is necessary because to execute snaps 239 // from strict snaps, we need to be able read and map 240 // /usr/lib/snapd/snap-confine from inside the strict snap mount namespace, 241 // even though /usr/lib/snapd/snap-confine from inside the strict snap mount 242 // namespace is actually a bind mount to the "snapConfineInCore" 243 patchedProfileText = bytes.Replace( 244 patchedProfileText, []byte("#@VERBATIM_LIBEXECDIR_SNAP_CONFINE@"), []byte("/usr/lib/snapd/snap-confine"), -1) 245 246 // We need to add a unique prefix that can never collide with a 247 // snap on the system. Using "snap-confine.*" is similar to 248 // "snap-update-ns.*" that is already used there 249 // 250 // So 251 // /snap/core/111/usr/lib/snapd/snap-confine 252 // becomes 253 // snap-confine.core.111 254 patchedProfileName := snapConfineProfileName(info.InstanceName(), info.Revision) 255 // remove other generated profiles, which is only relevant for the 256 // 'core' snap on classic system where we reexec, on core systems the 257 // profile is already a part of the rootfs snap 258 patchedProfileGlob := fmt.Sprintf("snap-confine.%s.*", info.InstanceName()) 259 260 if info.Type() == snap.TypeSnapd { 261 // with the snapd snap, things are a little different, the 262 // profile is discarded only late for the revisions that are 263 // being removed, also on core devices the rootfs snap and the 264 // snapd snap are updated separately, so the profile needs to be 265 // around for as long as the given revision of the snapd snap is 266 // active, so we use the exact match such that we only replace 267 // our own profile, which can happen if system was rebooted 268 // before task calling the backend was finished 269 patchedProfileGlob = patchedProfileName 270 } 271 272 // Return information for EnsureDirState that describes the re-exec profile for snap-confine. 273 content = map[string]osutil.FileState{ 274 patchedProfileName: &osutil.MemoryFileState{ 275 Content: []byte(patchedProfileText), 276 Mode: 0644, 277 }, 278 } 279 280 return dirs.SnapAppArmorDir, patchedProfileGlob, content, nil 281 } 282 283 func snapConfineProfileName(snapName string, rev snap.Revision) string { 284 return fmt.Sprintf("snap-confine.%s.%s", snapName, rev) 285 } 286 287 // setupSnapConfineReexec will setup apparmor profiles inside the host's 288 // /var/lib/snapd/apparmor/profiles directory. This is needed for 289 // running snap-confine from the core or snapd snap. 290 // 291 // Additionally it will cleanup stale apparmor profiles it created. 292 func (b *Backend) setupSnapConfineReexec(info *snap.Info) error { 293 if err := os.MkdirAll(dirs.SnapConfineAppArmorDir, 0755); err != nil { 294 return fmt.Errorf("cannot create snap-confine policy directory: %s", err) 295 } 296 dir, glob, content, err := snapConfineFromSnapProfile(info) 297 cache := apparmor_sandbox.CacheDir 298 if err != nil { 299 return fmt.Errorf("cannot compute snap-confine profile: %s", err) 300 } 301 if err := os.MkdirAll(dir, 0755); err != nil { 302 return fmt.Errorf("cannot create snap-confine directory %q: %s", dir, err) 303 } 304 305 changed, removed, errEnsure := osutil.EnsureDirState(dir, glob, content) 306 if len(changed) == 0 { 307 // XXX: because NFS workaround is handled separately the same correct 308 // snap-confine profile may need to be re-loaded. This is because the 309 // profile contains include directives and those load a second file 310 // that has changed outside of the scope of EnsureDirState. 311 // 312 // To counter that, always reload the profile by pretending it had 313 // changed. 314 for fname := range content { 315 changed = append(changed, fname) 316 } 317 } 318 pathnames := make([]string, len(changed)) 319 for i, profile := range changed { 320 pathnames[i] = filepath.Join(dir, profile) 321 } 322 323 var aaFlags apparmor_sandbox.AaParserFlags 324 if b.preseed { 325 aaFlags = apparmor_sandbox.SkipKernelLoad 326 } 327 errReload := loadProfiles(pathnames, cache, aaFlags) 328 errUnload := unloadProfiles(removed, cache) 329 if errEnsure != nil { 330 return fmt.Errorf("cannot synchronize snap-confine apparmor profile: %s", errEnsure) 331 } 332 if errReload != nil { 333 return fmt.Errorf("cannot reload snap-confine apparmor profile: %s", errReload) 334 } 335 if errUnload != nil { 336 return fmt.Errorf("cannot unload snap-confine apparmor profile: %s", errReload) 337 } 338 return nil 339 } 340 341 // nsProfile returns name of the apparmor profile for snap-update-ns for a given snap. 342 func nsProfile(snapName string) string { 343 return fmt.Sprintf("snap-update-ns.%s", snapName) 344 } 345 346 // profileGlobs returns a list of globs that describe the apparmor profiles of 347 // a given snap. 348 // 349 // Currently the list is just a pair. The first glob describes profiles for all 350 // apps and hooks while the second profile describes the snap-update-ns profile 351 // for the whole snap. 352 func profileGlobs(snapName string) []string { 353 return []string{interfaces.SecurityTagGlob(snapName), nsProfile(snapName)} 354 } 355 356 // Determine if a profile filename is removable during core refresh/rollback. 357 // This is needed because core devices are also special, the apparmor cache 358 // gets confused too easy, especially at rollbacks, so we delete the cache. See 359 // Setup(), below. Some systems employ a unified cache directory where all 360 // apparmor cache files are stored under one location so ensure we don't remove 361 // the snap profiles since snapd manages them elsewhere and instead only remove 362 // snap-confine and system profiles (eg, as shipped by distro package manager 363 // or created by the administrator). snap-confine profiles are like the 364 // following: 365 // - usr.lib.snapd.snap-confine.real 366 // - usr.lib.snapd.snap-confine (historic) 367 // - snap.core.NNNN.usr.lib.snapd.snap-confine (historic) 368 // - var.lib.snapd.snap.core.NNNN.usr.lib.snapd.snap-confine (historic) 369 // - snap-confine.core.NNNN 370 // - snap-confine.snapd.NNNN 371 func profileIsRemovableOnCoreSetup(fn string) bool { 372 bn := path.Base(fn) 373 if strings.HasPrefix(bn, ".") { 374 return false 375 } else if strings.HasPrefix(bn, "snap") && !strings.HasPrefix(bn, "snap-confine.core.") && !strings.HasPrefix(bn, "snap-confine.snapd.") && !strings.Contains(bn, "usr.lib.snapd.snap-confine") { 376 return false 377 } 378 return true 379 } 380 381 type profilePathsResults struct { 382 changed []string 383 unchanged []string 384 removed []string 385 } 386 387 func (b *Backend) prepareProfiles(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) (prof *profilePathsResults, err error) { 388 snapName := snapInfo.InstanceName() 389 spec, err := repo.SnapSpecification(b.Name(), snapName) 390 if err != nil { 391 return nil, fmt.Errorf("cannot obtain apparmor specification for snap %q: %s", snapName, err) 392 } 393 394 // Add snippets for parallel snap installation mapping 395 spec.(*Specification).AddOvername(snapInfo) 396 397 // Add snippets derived from the layout definition. 398 spec.(*Specification).AddLayout(snapInfo) 399 400 // Add additional mount layouts rules for the snap. 401 spec.(*Specification).AddExtraLayouts(snapInfo, opts.ExtraLayouts) 402 403 // core on classic is special 404 if snapName == "core" && release.OnClassic && apparmor_sandbox.ProbedLevel() != apparmor_sandbox.Unsupported { 405 if err := b.setupSnapConfineReexec(snapInfo); err != nil { 406 return nil, fmt.Errorf("cannot create host snap-confine apparmor configuration: %s", err) 407 } 408 } 409 410 // Deal with the "snapd" snap - we do the setup slightly differently 411 // here because this will run both on classic and on Ubuntu Core 18 412 // systems but /etc/apparmor.d is not writable on core18 systems 413 if snapInfo.Type() == snap.TypeSnapd && apparmor_sandbox.ProbedLevel() != apparmor_sandbox.Unsupported { 414 if err := b.setupSnapConfineReexec(snapInfo); err != nil { 415 return nil, fmt.Errorf("cannot create host snap-confine apparmor configuration: %s", err) 416 } 417 } 418 419 // core on core devices is also special, the apparmor cache gets 420 // confused too easy, especially at rollbacks, so we delete the cache. 421 // See LP:#1460152 and 422 // https://forum.snapcraft.io/t/core-snap-revert-issues-on-core-devices/ 423 // 424 if (snapInfo.Type() == snap.TypeOS || snapInfo.Type() == snap.TypeSnapd) && !release.OnClassic { 425 if li, err := filepath.Glob(filepath.Join(apparmor_sandbox.SystemCacheDir, "*")); err == nil { 426 for _, p := range li { 427 if st, err := os.Stat(p); err == nil && st.Mode().IsRegular() && profileIsRemovableOnCoreSetup(p) { 428 if err := os.Remove(p); err != nil { 429 logger.Noticef("cannot remove %q: %s", p, err) 430 } 431 } 432 } 433 } 434 } 435 436 // Get the files that this snap should have 437 content := b.deriveContent(spec.(*Specification), snapInfo, opts) 438 439 dir := dirs.SnapAppArmorDir 440 globs := profileGlobs(snapInfo.InstanceName()) 441 if err := os.MkdirAll(dir, 0755); err != nil { 442 return nil, fmt.Errorf("cannot create directory for apparmor profiles %q: %s", dir, err) 443 } 444 changed, removedPaths, errEnsure := osutil.EnsureDirStateGlobs(dir, globs, content) 445 // XXX: in the old code this error was reported late, after doing load/unload. 446 if errEnsure != nil { 447 return nil, fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, errEnsure) 448 } 449 450 // Find the set of unchanged profiles. 451 unchanged := make([]string, 0, len(content)-len(changed)) 452 for name := range content { 453 // changed is pre-sorted by EnsureDirStateGlobs 454 x := sort.SearchStrings(changed, name) 455 if x < len(changed) && changed[x] == name { 456 continue 457 } 458 unchanged = append(unchanged, name) 459 } 460 sort.Strings(unchanged) 461 462 changedPaths := make([]string, len(changed)) 463 for i, profile := range changed { 464 changedPaths[i] = filepath.Join(dir, profile) 465 } 466 467 unchangedPaths := make([]string, len(unchanged)) 468 for i, profile := range unchanged { 469 unchangedPaths[i] = filepath.Join(dir, profile) 470 } 471 472 return &profilePathsResults{changed: changedPaths, removed: removedPaths, unchanged: unchangedPaths}, nil 473 } 474 475 // Setup creates and loads apparmor profiles specific to a given snap. 476 // The snap can be in developer mode to make security violations non-fatal to 477 // the offending application process. 478 // 479 // This method should be called after changing plug, slots, connections between 480 // them or application present in the snap. 481 func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) error { 482 prof, err := b.prepareProfiles(snapInfo, opts, repo) 483 if err != nil { 484 return err 485 } 486 487 // Load all changed profiles with a flag that asks apparmor to skip reading 488 // the cache (since we know those changed for sure). This allows us to 489 // work despite time being wrong (e.g. in the past). For more details see 490 // https://forum.snapcraft.io/t/apparmor-profile-caching/1268/18 491 var errReloadChanged error 492 aaFlags := apparmor_sandbox.SkipReadCache 493 if b.preseed { 494 aaFlags |= apparmor_sandbox.SkipKernelLoad 495 } 496 timings.Run(tm, "load-profiles[changed]", fmt.Sprintf("load changed security profiles of snap %q", snapInfo.InstanceName()), func(nesttm timings.Measurer) { 497 errReloadChanged = loadProfiles(prof.changed, apparmor_sandbox.CacheDir, aaFlags) 498 }) 499 500 // Load all unchanged profiles anyway. This ensures those are correct in 501 // the kernel even if the files on disk were not changed. We rely on 502 // apparmor cache to make this performant. 503 var errReloadOther error 504 aaFlags = 0 505 if b.preseed { 506 aaFlags |= apparmor_sandbox.SkipKernelLoad 507 } 508 timings.Run(tm, "load-profiles[unchanged]", fmt.Sprintf("load unchanged security profiles of snap %q", snapInfo.InstanceName()), func(nesttm timings.Measurer) { 509 errReloadOther = loadProfiles(prof.unchanged, apparmor_sandbox.CacheDir, aaFlags) 510 }) 511 errUnload := unloadProfiles(prof.removed, apparmor_sandbox.CacheDir) 512 if errReloadChanged != nil { 513 return errReloadChanged 514 } 515 if errReloadOther != nil { 516 return errReloadOther 517 } 518 return errUnload 519 } 520 521 // SetupMany creates and loads apparmor profiles for multiple snaps. 522 // The snaps can be in developer mode to make security violations non-fatal to 523 // the offending application process. 524 // SetupMany tries to recreate all profiles without interrupting on errors, but 525 // collects and returns them all. 526 // 527 // This method is useful mainly for regenerating profiles. 528 func (b *Backend) SetupMany(snaps []*snap.Info, confinement func(snapName string) interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) []error { 529 var allChangedPaths, allUnchangedPaths, allRemovedPaths []string 530 var fallback bool 531 for _, snapInfo := range snaps { 532 opts := confinement(snapInfo.InstanceName()) 533 prof, err := b.prepareProfiles(snapInfo, opts, repo) 534 if err != nil { 535 fallback = true 536 break 537 } 538 allChangedPaths = append(allChangedPaths, prof.changed...) 539 allUnchangedPaths = append(allUnchangedPaths, prof.unchanged...) 540 allRemovedPaths = append(allRemovedPaths, prof.removed...) 541 } 542 543 if !fallback { 544 aaFlags := apparmor_sandbox.SkipReadCache | apparmor_sandbox.ConserveCPU 545 if b.preseed { 546 aaFlags |= apparmor_sandbox.SkipKernelLoad 547 } 548 var errReloadChanged error 549 timings.Run(tm, "load-profiles[changed-many]", fmt.Sprintf("load changed security profiles of %d snaps", len(snaps)), func(nesttm timings.Measurer) { 550 errReloadChanged = loadProfiles(allChangedPaths, apparmor_sandbox.CacheDir, aaFlags) 551 }) 552 553 aaFlags = apparmor_sandbox.ConserveCPU 554 if b.preseed { 555 aaFlags |= apparmor_sandbox.SkipKernelLoad 556 } 557 var errReloadOther error 558 timings.Run(tm, "load-profiles[unchanged-many]", fmt.Sprintf("load unchanged security profiles %d snaps", len(snaps)), func(nesttm timings.Measurer) { 559 errReloadOther = loadProfiles(allUnchangedPaths, apparmor_sandbox.CacheDir, aaFlags) 560 }) 561 562 errUnload := unloadProfiles(allRemovedPaths, apparmor_sandbox.CacheDir) 563 if errReloadChanged != nil { 564 logger.Noticef("failed to batch-reload changed profiles: %s", errReloadChanged) 565 fallback = true 566 } 567 if errReloadOther != nil { 568 logger.Noticef("failed to batch-reload unchanged profiles: %s", errReloadOther) 569 fallback = true 570 } 571 if errUnload != nil { 572 logger.Noticef("failed to batch-unload profiles: %s", errUnload) 573 fallback = true 574 } 575 } 576 577 var errors []error 578 // if an error was encountered when processing all profiles at once, re-try them one by one 579 if fallback { 580 for _, snapInfo := range snaps { 581 opts := confinement(snapInfo.InstanceName()) 582 if err := b.Setup(snapInfo, opts, repo, tm); err != nil { 583 errors = append(errors, fmt.Errorf("cannot setup profiles for snap %q: %s", snapInfo.InstanceName(), err)) 584 } 585 } 586 } 587 return errors 588 } 589 590 // Remove removes and unloads apparmor profiles of a given snap. 591 func (b *Backend) Remove(snapName string) error { 592 dir := dirs.SnapAppArmorDir 593 globs := profileGlobs(snapName) 594 cache := apparmor_sandbox.CacheDir 595 _, removed, errEnsure := osutil.EnsureDirStateGlobs(dir, globs, nil) 596 // always try to unload affected profiles 597 errUnload := unloadProfiles(removed, cache) 598 if errEnsure != nil { 599 return fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, errEnsure) 600 } 601 return errUnload 602 } 603 604 func (b *Backend) RemoveLate(snapName string, rev snap.Revision, typ snap.Type) error { 605 logger.Debugf("remove late for snap %v (%s) type %v", snapName, rev, typ) 606 if typ != snap.TypeSnapd { 607 // late remove is relevant only for snap confine profiles 608 return nil 609 } 610 611 globs := []string{snapConfineProfileName(snapName, rev)} 612 _, removed, errEnsure := osutil.EnsureDirStateGlobs(dirs.SnapAppArmorDir, globs, nil) 613 // XXX: unloadProfiles() does not unload profiles from the kernel, but 614 // only removes profiles from the cache 615 // always try to unload the affected profile 616 errUnload := unloadProfiles(removed, apparmor_sandbox.CacheDir) 617 if errEnsure != nil { 618 return fmt.Errorf("cannot remove security profiles for snap %q (%s): %s", snapName, rev, errEnsure) 619 } 620 return errUnload 621 } 622 623 var ( 624 templatePattern = regexp.MustCompile("(###[A-Z_]+###)") 625 coreRuntimePattern = regexp.MustCompile("^core([0-9][0-9])?$") 626 ) 627 628 const ( 629 attachPattern = "(attach_disconnected,mediate_deleted)" 630 attachComplain = "(attach_disconnected,mediate_deleted,complain)" 631 ) 632 633 func (b *Backend) deriveContent(spec *Specification, snapInfo *snap.Info, opts interfaces.ConfinementOptions) (content map[string]osutil.FileState) { 634 content = make(map[string]osutil.FileState, len(snapInfo.Apps)+len(snapInfo.Hooks)+1) 635 636 // Add profile for each app. 637 for _, appInfo := range snapInfo.Apps { 638 securityTag := appInfo.SecurityTag() 639 b.addContent(securityTag, snapInfo, appInfo.Name, opts, spec.SnippetForTag(securityTag), content, spec) 640 } 641 // Add profile for each hook. 642 for _, hookInfo := range snapInfo.Hooks { 643 securityTag := hookInfo.SecurityTag() 644 b.addContent(securityTag, snapInfo, "hook."+hookInfo.Name, opts, spec.SnippetForTag(securityTag), content, spec) 645 } 646 // Add profile for snap-update-ns if we have any apps or hooks. 647 // If we have neither then we don't have any need to create an executing environment. 648 // This applies to, for example, kernel snaps or gadget snaps (unless they have hooks). 649 if len(content) > 0 { 650 snippets := strings.Join(spec.UpdateNS(), "\n") 651 addUpdateNSProfile(snapInfo, snippets, content) 652 } 653 654 return content 655 } 656 657 // addUpdateNSProfile adds an apparmor profile for snap-update-ns, tailored to a specific snap. 658 // 659 // This profile exists so that snap-update-ns doens't need to carry very wide, open permissions 660 // that are suitable for poking holes (and writing) in nearly arbitrary places. Instead the profile 661 // contains just the permissions needed to poke a hole and write to the layout-specific paths. 662 func addUpdateNSProfile(snapInfo *snap.Info, snippets string, content map[string]osutil.FileState) { 663 // Compute the template by injecting special updateNS snippets. 664 policy := templatePattern.ReplaceAllStringFunc(updateNSTemplate, func(placeholder string) string { 665 switch placeholder { 666 case "###SNAP_INSTANCE_NAME###": 667 return snapInfo.InstanceName() 668 case "###SNIPPETS###": 669 if overlayRoot, _ := isRootWritableOverlay(); overlayRoot != "" { 670 snippets += strings.Replace(overlayRootSnippet, "###UPPERDIR###", overlayRoot, -1) 671 } 672 return snippets 673 } 674 return "" 675 }) 676 677 // Ensure that the snap-update-ns profile is on disk. 678 profileName := nsProfile(snapInfo.InstanceName()) 679 content[profileName] = &osutil.MemoryFileState{ 680 Content: []byte(policy), 681 Mode: 0644, 682 } 683 } 684 685 func (b *Backend) addContent(securityTag string, snapInfo *snap.Info, cmdName string, opts interfaces.ConfinementOptions, snippetForTag string, content map[string]osutil.FileState, spec *Specification) { 686 // If base is specified and it doesn't match the core snaps (not 687 // specifying a base should use the default core policy since in this 688 // case, the 'core' snap is used for the runtime), use the base 689 // apparmor template, otherwise use the default template. 690 var policy string 691 if snapInfo.Base != "" && !coreRuntimePattern.MatchString(snapInfo.Base) { 692 policy = defaultOtherBaseTemplate 693 } else { 694 policy = defaultCoreRuntimeTemplate 695 } 696 697 ignoreSnippets := false 698 // Classic confinement (unless overridden by JailMode) has a dedicated 699 // permissive template that applies a strict, but very open, policy. 700 if opts.Classic && !opts.JailMode { 701 policy = classicTemplate 702 ignoreSnippets = true 703 } 704 // If a snap is in devmode (or is using classic confinement) then make the 705 // profile non-enforcing where violations are logged but not denied. 706 // This is also done for classic so that no confinement applies. Just in 707 // case the profile we start with is not permissive enough. 708 if (opts.DevMode || opts.Classic) && !opts.JailMode { 709 policy = strings.Replace(policy, attachPattern, attachComplain, -1) 710 } 711 policy = templatePattern.ReplaceAllStringFunc(policy, func(placeholder string) string { 712 switch placeholder { 713 case "###DEVMODE_SNAP_CONFINE###": 714 if !opts.DevMode { 715 // nothing to add if we are not in devmode 716 return "" 717 } 718 719 // otherwise we need to generate special policy to allow executing 720 // snap-confine from inside a devmode snap 721 722 // TODO: we should deprecate this and drop it in a future release 723 724 // assumes coreSnapInfo is not nil 725 coreProfileTarget := func() string { 726 return fmt.Sprintf("/snap/core/%s/usr/lib/snapd/snap-confine", b.coreSnap.SnapRevision().String()) 727 } 728 729 // assumes snapdSnapInfo is not nil 730 snapdProfileTarget := func() string { 731 return fmt.Sprintf("/snap/snapd/%s/usr/lib/snapd/snap-confine", b.snapdSnap.SnapRevision().String()) 732 } 733 734 // There are 3 main apparmor exec transition rules we need to 735 // generate: 736 // * exec( /usr/lib/snapd/snap-confine ... ) 737 // * exec( /snap/snapd/<rev>/usr/lib/snapd/snap-confine ... ) 738 // * exec( /snap/core/<rev>/usr/lib/snapd/snap-confine ... ) 739 740 // The latter two can always transition to their respective 741 // revisioned profiles unambiguously if each snap is installed. 742 743 // The former rule for /usr/lib/snapd/snap-confine however is 744 // more tricky. First, we can say that if only the snapd snap is 745 // installed, to just transition to that profile and be done. If 746 // just the core snap is installed, then we can deduce this 747 // system is either UC16 or a classic one, in both cases though 748 // we have /usr/lib/snapd/snap-confine defined as the profile to 749 // transition to. 750 // If both snaps are installed however, then we need to branch 751 // and pick a profile that exists, we can't just arbitrarily 752 // pick one profile because not all profiles will exist on all 753 // systems actually, for example the snap-confine profile from 754 // the core snap will not be generated/installed on UC18+. We 755 // can simplify the logic however by realizing that no matter 756 // the relative version numbers of snapd and core, when 757 // executing a snap with base other than core (i.e. base core18 758 // or core20), the snapd snap's version of snap-confine will 759 // always be used for various reasons. This is also true for 760 // base: core snaps, but only on non-classic systems. So we 761 // essentially say that /usr/lib/snapd/snap-confine always 762 // transitions to the snapd snap profile if the base is not 763 // core or if the system is not classic. If the base is core and 764 // the system is classic, then the core snap profile will be 765 // used. 766 767 usrLibSnapdConfineTransitionTarget := "" 768 switch { 769 case b.coreSnap != nil && b.snapdSnap == nil: 770 // only core snap - use /usr/lib/snapd/snap-confine always 771 usrLibSnapdConfineTransitionTarget = "/usr/lib/snapd/snap-confine" 772 case b.snapdSnap != nil && b.coreSnap == nil: 773 // only snapd snap - use snapd snap version 774 usrLibSnapdConfineTransitionTarget = snapdProfileTarget() 775 case b.snapdSnap != nil && b.coreSnap != nil: 776 // both are installed - need to check which one to use 777 // note that a base of "core" is represented by base == "" for 778 // historical reasons 779 if release.OnClassic && snapInfo.Base == "" { 780 // use the core snap as the target only if we are on 781 // classic and the base is core 782 usrLibSnapdConfineTransitionTarget = coreProfileTarget() 783 } else { 784 // otherwise always use snapd 785 usrLibSnapdConfineTransitionTarget = snapdProfileTarget() 786 } 787 788 default: 789 // neither of the snaps are installed 790 791 // TODO: this panic is unfortunate, but we don't have time 792 // to do any better for this security release 793 // It is actually important that we panic here, the only 794 // known circumstance where this happens is when we are 795 // seeding during first boot of UC16 with a very new core 796 // snap (i.e. with the security fix of 2.54.3) and also have 797 // a devmode confined snap in the seed to prepare. In this 798 // situation, when we panic(), we force snapd to exit, and 799 // systemd will restart us and we actually recover the 800 // initial seed change and continue on. This code will be 801 // removed/adapted before it is merged to the main branch, 802 // it is only meant to exist on the security release branch. 803 msg := fmt.Sprintf("neither snapd nor core snap available while preparing apparmor profile for devmode snap %s, panicing to restart snapd to continue seeding", snapInfo.InstanceName()) 804 panic(msg) 805 } 806 807 // We use Pxr for all these rules since the snap-confine profile 808 // is not a child profile of the devmode complain profile we are 809 // generating right now. 810 usrLibSnapdConfineTransitionRule := fmt.Sprintf("/usr/lib/snapd/snap-confine Pxr -> %s,\n", usrLibSnapdConfineTransitionTarget) 811 812 coreSnapConfineSnippet := "" 813 if b.coreSnap != nil { 814 coreSnapConfineSnippet = fmt.Sprintf("/snap/core/*/usr/lib/snapd/snap-confine Pxr -> %s,\n", coreProfileTarget()) 815 } 816 817 snapdSnapConfineSnippet := "" 818 if b.snapdSnap != nil { 819 snapdSnapConfineSnippet = fmt.Sprintf("/snap/snapd/*/usr/lib/snapd/snap-confine Pxr -> %s,\n", snapdProfileTarget()) 820 } 821 822 nonBaseCoreTransitionSnippet := coreSnapConfineSnippet + "\n" + snapdSnapConfineSnippet 823 824 // include both rules for the core snap and the snapd snap since 825 // we can't know which one will be used at runtime (for example 826 // SNAP_REEXEC could be set which affects which one is used) 827 return fmt.Sprintf(` 828 # allow executing the snap command from either the rootfs (for base: core) or 829 # from the system snaps (all other bases) - this is very specifically only to 830 # enable proper apparmor profile transition to snap-confine below, if we don't 831 # include these exec rules, then when executing the snap command, apparmor 832 # will create a new, unique sub-profile which then cannot be transitioned from 833 # to the actual snap-confine profile 834 /usr/bin/snap ixr, 835 /snap/{snapd,core}/*/usr/bin/snap ixr, 836 837 # allow transitioning to snap-confine to support executing strict snaps from 838 # inside devmode confined snaps 839 840 # this first rule is to handle the case of exec()ing 841 # /usr/lib/snapd/snap-confine directly, the profile we transition to depends 842 # on whether we are classic or not, what snaps (snapd or core) are installed 843 # and also whether this snap is a base: core snap or a differently based snap. 844 # see the comment in interfaces/backend/apparmor.go where this snippet is 845 # generated for the full context 846 %[1]s 847 848 # the second (and possibly third if both core and snapd are installed) rule is 849 # to handle direct exec() of snap-confine from the respective snaps directly, 850 # this happens mostly on non-core based snaps, wherein the base snap has a 851 # symlink from /usr/bin/snap -> /snap/snapd/current/usr/bin/snap, which makes 852 # the snap command execute snap-confine directly from the associated system 853 # snap in /snap/{snapd,core}/<rev>/usr/lib/snapd/snap-confine 854 %[2]s 855 `, usrLibSnapdConfineTransitionRule, nonBaseCoreTransitionSnippet) 856 857 case "###VAR###": 858 return templateVariables(snapInfo, securityTag, cmdName) 859 case "###PROFILEATTACH###": 860 return fmt.Sprintf("profile \"%s\"", securityTag) 861 case "###CHANGEPROFILE_RULE###": 862 features, _ := parserFeatures() 863 for _, f := range features { 864 if f == "unsafe" { 865 return "change_profile unsafe /**," 866 } 867 } 868 return "change_profile," 869 case "###SNIPPETS###": 870 var tagSnippets string 871 if opts.Classic && opts.JailMode { 872 // Add a special internal snippet for snaps using classic confinement 873 // and jailmode together. This snippet provides access to the core snap 874 // so that the dynamic linker and shared libraries can be used. 875 tagSnippets = classicJailmodeSnippet + "\n" + snippetForTag 876 } else if ignoreSnippets { 877 // When classic confinement template is in effect we are 878 // ignoring all apparmor snippets as they may conflict with the 879 // super-broad template we are starting with. 880 } else { 881 // Check if NFS is mounted at or under $HOME. Because NFS is not 882 // transparent to apparmor we must alter the profile to counter that and 883 // allow access to SNAP_USER_* files. 884 tagSnippets = snippetForTag 885 if nfs, _ := isHomeUsingNFS(); nfs { 886 tagSnippets += nfsSnippet 887 } 888 889 if overlayRoot, _ := isRootWritableOverlay(); overlayRoot != "" { 890 snippet := strings.Replace(overlayRootSnippet, "###UPPERDIR###", overlayRoot, -1) 891 tagSnippets += snippet 892 } 893 894 // Add core specific snippets when not on classic 895 if !release.OnClassic { 896 tagSnippets += coreSnippet 897 } 898 } 899 900 if !ignoreSnippets { 901 // For policy with snippets that request 902 // suppression of 'ptrace (trace)' denials, add 903 // the suppression rule unless another 904 // interface said it uses them. 905 if spec.SuppressPtraceTrace() && !spec.UsesPtraceTrace() { 906 tagSnippets += ptraceTraceDenySnippet 907 } 908 909 // Deny the sys_module capability unless it has been explicitly 910 // requested 911 if spec.SuppressSysModuleCapability() && !spec.UsesSysModuleCapability() { 912 tagSnippets += sysModuleCapabilityDenySnippet 913 } 914 915 // Use 'ix' rules in the home interface unless an 916 // interface asked to suppress them 917 repl := "ix" 918 if spec.SuppressHomeIx() { 919 repl = "" 920 } 921 tagSnippets = strings.Replace(tagSnippets, "###HOME_IX###", repl, -1) 922 923 // Conditionally add privilege dropping policy 924 if len(snapInfo.SystemUsernames) > 0 { 925 tagSnippets += privDropAndChownRules 926 } 927 } 928 929 return tagSnippets 930 } 931 return "" 932 }) 933 934 content[securityTag] = &osutil.MemoryFileState{ 935 Content: []byte(policy), 936 Mode: 0644, 937 } 938 } 939 940 // NewSpecification returns a new, empty apparmor specification. 941 func (b *Backend) NewSpecification() interfaces.Specification { 942 return &Specification{} 943 } 944 945 // SandboxFeatures returns the list of apparmor features supported by the kernel. 946 func (b *Backend) SandboxFeatures() []string { 947 if apparmor_sandbox.ProbedLevel() == apparmor_sandbox.Unsupported { 948 return nil 949 } 950 951 kFeatures, _ := kernelFeatures() 952 pFeatures, _ := parserFeatures() 953 tags := make([]string, 0, len(kFeatures)+len(pFeatures)) 954 for _, feature := range kFeatures { 955 // Prepend "kernel:" to apparmor kernel features to namespace them and 956 // allow us to introduce our own tags later. 957 tags = append(tags, "kernel:"+feature) 958 } 959 960 for _, feature := range pFeatures { 961 // Prepend "parser:" to apparmor kernel features to namespace 962 // them and allow us to introduce our own tags later. 963 tags = append(tags, "parser:"+feature) 964 } 965 966 level := "full" 967 policy := "default" 968 if apparmor_sandbox.ProbedLevel() == apparmor_sandbox.Partial { 969 level = "partial" 970 } 971 tags = append(tags, fmt.Sprintf("support-level:%s", level)) 972 tags = append(tags, fmt.Sprintf("policy:%s", policy)) 973 974 return tags 975 } 976 977 // MockIsHomeUsingNFS mocks the real implementation of osutil.IsHomeUsingNFS. 978 // This is exported so that other packages that indirectly interact with AppArmor backend 979 // can mock isHomeUsingNFS. 980 func MockIsHomeUsingNFS(new func() (bool, error)) (restore func()) { 981 old := isHomeUsingNFS 982 isHomeUsingNFS = new 983 return func() { 984 isHomeUsingNFS = old 985 } 986 }