github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 "github.com/snapcore/snapd/dirs" 52 "github.com/snapcore/snapd/interfaces" 53 "github.com/snapcore/snapd/logger" 54 "github.com/snapcore/snapd/osutil" 55 "github.com/snapcore/snapd/release" 56 apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" 57 "github.com/snapcore/snapd/snap" 58 "github.com/snapcore/snapd/strutil" 59 "github.com/snapcore/snapd/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 ) 69 70 // Backend is responsible for maintaining apparmor profiles for snaps and parts of snapd. 71 type Backend struct { 72 preseed bool 73 } 74 75 // Name returns the name of the backend. 76 func (b *Backend) Name() interfaces.SecuritySystem { 77 return interfaces.SecurityAppArmor 78 } 79 80 // Initialize prepares customized apparmor policy for snap-confine. 81 func (b *Backend) Initialize(opts *interfaces.SecurityBackendOptions) error { 82 if opts != nil && opts.Preseed { 83 b.preseed = true 84 } 85 // NOTE: It would be nice if we could also generate the profile for 86 // snap-confine executing from the core snap, right here, and not have to 87 // do this in the Setup function below. I sadly don't think this is 88 // possible because snapd must be able to install a new core and only at 89 // that moment generate it. 90 91 // Inspect the system and sets up local apparmor policy for snap-confine. 92 // Local policy is included by the system-wide policy. If the local policy 93 // has changed then the apparmor profile for snap-confine is reloaded. 94 95 // Create the local policy directory if it is not there. 96 if err := os.MkdirAll(dirs.SnapConfineAppArmorDir, 0755); err != nil { 97 return fmt.Errorf("cannot create snap-confine policy directory: %s", err) 98 } 99 100 // Check the /proc/self/exe symlink, this is needed below but we want to 101 // fail early if this fails for whatever reason. 102 exe, err := os.Readlink(procSelfExe) 103 if err != nil { 104 return fmt.Errorf("cannot read %s: %s", procSelfExe, err) 105 } 106 107 // Location of the generated policy. 108 glob := "*" 109 policy := make(map[string]osutil.FileState) 110 111 // Check if NFS is mounted at or under $HOME. Because NFS is not 112 // transparent to apparmor we must alter our profile to counter that and 113 // allow snap-confine to work. 114 if nfs, err := isHomeUsingNFS(); err != nil { 115 logger.Noticef("cannot determine if NFS is in use: %v", err) 116 } else if nfs { 117 policy["nfs-support"] = &osutil.MemoryFileState{ 118 Content: []byte(nfsSnippet), 119 Mode: 0644, 120 } 121 logger.Noticef("snapd enabled NFS support, additional implicit network permissions granted") 122 } 123 124 // Check if '/' is on overlayfs. If so, add the necessary rules for 125 // upperdir and allow snap-confine to work. 126 if overlayRoot, err := isRootWritableOverlay(); err != nil { 127 logger.Noticef("cannot determine if root filesystem on overlay: %v", err) 128 } else if overlayRoot != "" { 129 snippet := strings.Replace(overlayRootSnippet, "###UPPERDIR###", overlayRoot, -1) 130 policy["overlay-root"] = &osutil.MemoryFileState{ 131 Content: []byte(snippet), 132 Mode: 0644, 133 } 134 logger.Noticef("snapd enabled root filesystem on overlay support, additional upperdir permissions granted") 135 } 136 137 // Ensure that generated policy is what we computed above. 138 created, removed, err := osutil.EnsureDirState(dirs.SnapConfineAppArmorDir, glob, policy) 139 if err != nil { 140 return fmt.Errorf("cannot synchronize snap-confine policy: %s", err) 141 } 142 if len(created) == 0 && len(removed) == 0 { 143 // If the generated policy didn't change, we're all done. 144 return nil 145 } 146 147 // If snapd is executing from the core snap the it means it has 148 // re-executed. In that case we are no longer using the copy of 149 // snap-confine from the host distribution but our own copy. We don't have 150 // to re-compile and load the updated profile as that is performed by 151 // setupSnapConfineReexec below. 152 if strings.HasPrefix(exe, dirs.SnapMountDir) { 153 return nil 154 } 155 156 // Reload the apparmor profile of snap-confine. This points to the main 157 // file in /etc/apparmor.d/ as that file contains include statements that 158 // load any of the files placed in /var/lib/snapd/apparmor/snap-confine/. 159 // For historical reasons we may have a filename that ends with .real or 160 // not. If we do then we prefer the file ending with the name .real as 161 // that is the more recent name we use. 162 var profilePath string 163 for _, profileFname := range []string{"usr.lib.snapd.snap-confine.real", "usr.lib.snapd.snap-confine"} { 164 profilePath = filepath.Join(apparmor_sandbox.ConfDir, profileFname) 165 if _, err := os.Stat(profilePath); err != nil { 166 if os.IsNotExist(err) { 167 continue 168 } 169 return err 170 } 171 break 172 } 173 if profilePath == "" { 174 return fmt.Errorf("cannot find system apparmor profile for snap-confine") 175 } 176 177 aaFlags := skipReadCache 178 if b.preseed { 179 aaFlags |= skipKernelLoad 180 } 181 182 // We are not using apparmor.LoadProfiles() because it uses other cache. 183 if err := loadProfiles([]string{profilePath}, apparmor_sandbox.SystemCacheDir, aaFlags); err != nil { 184 // When we cannot reload the profile then let's remove the generated 185 // policy. Maybe we have caused the problem so it's better to let other 186 // things work. 187 osutil.EnsureDirState(dirs.SnapConfineAppArmorDir, glob, nil) 188 return fmt.Errorf("cannot reload snap-confine apparmor profile: %v", err) 189 } 190 return nil 191 } 192 193 // snapConfineFromSnapProfile returns the apparmor profile for 194 // snap-confine in the given core/snapd snap. 195 func snapConfineFromSnapProfile(info *snap.Info) (dir, glob string, content map[string]osutil.FileState, err error) { 196 // Find the vanilla apparmor profile for snap-confine as present in the given core snap. 197 198 // We must test the ".real" suffix first, this is a workaround for 199 // https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=858004 200 vanillaProfilePath := filepath.Join(info.MountDir(), "/etc/apparmor.d/usr.lib.snapd.snap-confine.real") 201 vanillaProfileText, err := ioutil.ReadFile(vanillaProfilePath) 202 if os.IsNotExist(err) { 203 vanillaProfilePath = filepath.Join(info.MountDir(), "/etc/apparmor.d/usr.lib.snapd.snap-confine") 204 vanillaProfileText, err = ioutil.ReadFile(vanillaProfilePath) 205 } 206 if err != nil { 207 return "", "", nil, fmt.Errorf("cannot open apparmor profile for vanilla snap-confine: %s", err) 208 } 209 210 // Replace the path to vanilla snap-confine with the path to the mounted snap-confine from core. 211 snapConfineInCore := filepath.Join(info.MountDir(), "usr/lib/snapd/snap-confine") 212 patchedProfileText := bytes.Replace( 213 vanillaProfileText, []byte("/usr/lib/snapd/snap-confine"), []byte(snapConfineInCore), -1) 214 215 // We need to add a uniqe prefix that can never collide with a 216 // snap on the system. Using "snap-confine.*" is similar to 217 // "snap-update-ns.*" that is already used there 218 // 219 // So 220 // /snap/core/111/usr/lib/snapd/snap-confine 221 // becomes 222 // snap-confine.core.111 223 patchedProfileName := fmt.Sprintf("snap-confine.%s.%s", info.InstanceName(), info.Revision) 224 patchedProfileGlob := fmt.Sprintf("snap-confine.%s.*", info.InstanceName()) 225 226 // Return information for EnsureDirState that describes the re-exec profile for snap-confine. 227 content = map[string]osutil.FileState{ 228 patchedProfileName: &osutil.MemoryFileState{ 229 Content: []byte(patchedProfileText), 230 Mode: 0644, 231 }, 232 } 233 234 return dirs.SnapAppArmorDir, patchedProfileGlob, content, nil 235 } 236 237 // setupSnapConfineReexec will setup apparmor profiles inside the host's 238 // /var/lib/snapd/apparmor/profiles directory. This is needed for 239 // running snap-confine from the core or snapd snap. 240 // 241 // Additionally it will cleanup stale apparmor profiles it created. 242 func (b *Backend) setupSnapConfineReexec(info *snap.Info) error { 243 if err := os.MkdirAll(dirs.SnapConfineAppArmorDir, 0755); err != nil { 244 return fmt.Errorf("cannot create snap-confine policy directory: %s", err) 245 } 246 dir, glob, content, err := snapConfineFromSnapProfile(info) 247 cache := apparmor_sandbox.CacheDir 248 if err != nil { 249 return fmt.Errorf("cannot compute snap-confine profile: %s", err) 250 } 251 if err := os.MkdirAll(dir, 0755); err != nil { 252 return fmt.Errorf("cannot create snap-confine directory %q: %s", dir, err) 253 } 254 255 changed, removed, errEnsure := osutil.EnsureDirState(dir, glob, content) 256 if len(changed) == 0 { 257 // XXX: because NFS workaround is handled separately the same correct 258 // snap-confine profile may need to be re-loaded. This is because the 259 // profile contains include directives and those load a second file 260 // that has changed outside of the scope of EnsureDirState. 261 // 262 // To counter that, always reload the profile by pretending it had 263 // changed. 264 for fname := range content { 265 changed = append(changed, fname) 266 } 267 } 268 pathnames := make([]string, len(changed)) 269 for i, profile := range changed { 270 pathnames[i] = filepath.Join(dir, profile) 271 } 272 273 var aaFlags aaParserFlags 274 if b.preseed { 275 aaFlags = skipKernelLoad 276 } 277 errReload := loadProfiles(pathnames, cache, aaFlags) 278 errUnload := unloadProfiles(removed, cache) 279 if errEnsure != nil { 280 return fmt.Errorf("cannot synchronize snap-confine apparmor profile: %s", errEnsure) 281 } 282 if errReload != nil { 283 return fmt.Errorf("cannot reload snap-confine apparmor profile: %s", errReload) 284 } 285 if errUnload != nil { 286 return fmt.Errorf("cannot unload snap-confine apparmor profile: %s", errReload) 287 } 288 return nil 289 } 290 291 // nsProfile returns name of the apparmor profile for snap-update-ns for a given snap. 292 func nsProfile(snapName string) string { 293 return fmt.Sprintf("snap-update-ns.%s", snapName) 294 } 295 296 // profileGlobs returns a list of globs that describe the apparmor profiles of 297 // a given snap. 298 // 299 // Currently the list is just a pair. The first glob describes profiles for all 300 // apps and hooks while the second profile describes the snap-update-ns profile 301 // for the whole snap. 302 func profileGlobs(snapName string) []string { 303 return []string{interfaces.SecurityTagGlob(snapName), nsProfile(snapName)} 304 } 305 306 // Determine if a profile filename is removable during core refresh/rollback. 307 // This is needed because core devices are also special, the apparmor cache 308 // gets confused too easy, especially at rollbacks, so we delete the cache. See 309 // Setup(), below. Some systems employ a unified cache directory where all 310 // apparmor cache files are stored under one location so ensure we don't remove 311 // the snap profiles since snapd manages them elsewhere and instead only remove 312 // snap-confine and system profiles (eg, as shipped by distro package manager 313 // or created by the administrator). snap-confine profiles are like the 314 // following: 315 // - usr.lib.snapd.snap-confine.real 316 // - usr.lib.snapd.snap-confine (historic) 317 // - snap.core.NNNN.usr.lib.snapd.snap-confine (historic) 318 // - var.lib.snapd.snap.core.NNNN.usr.lib.snapd.snap-confine (historic) 319 // - snap-confine.core.NNNN 320 // - snap-confine.snapd.NNNN 321 func profileIsRemovableOnCoreSetup(fn string) bool { 322 bn := path.Base(fn) 323 if strings.HasPrefix(bn, ".") { 324 return false 325 } 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") { 326 return false 327 } 328 return true 329 } 330 331 type profilePathsResults struct { 332 changed []string 333 unchanged []string 334 removed []string 335 } 336 337 func (b *Backend) prepareProfiles(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) (prof *profilePathsResults, err error) { 338 snapName := snapInfo.InstanceName() 339 spec, err := repo.SnapSpecification(b.Name(), snapName) 340 if err != nil { 341 return nil, fmt.Errorf("cannot obtain apparmor specification for snap %q: %s", snapName, err) 342 } 343 344 // Add snippets for parallel snap installation mapping 345 spec.(*Specification).AddOvername(snapInfo) 346 347 // Add snippets derived from the layout definition. 348 spec.(*Specification).AddLayout(snapInfo) 349 350 // core on classic is special 351 if snapName == "core" && release.OnClassic && apparmor_sandbox.ProbedLevel() != apparmor_sandbox.Unsupported { 352 if err := b.setupSnapConfineReexec(snapInfo); err != nil { 353 return nil, fmt.Errorf("cannot create host snap-confine apparmor configuration: %s", err) 354 } 355 } 356 357 // Deal with the "snapd" snap - we do the setup slightly differently 358 // here because this will run both on classic and on Ubuntu Core 18 359 // systems but /etc/apparmor.d is not writable on core18 systems 360 if snapInfo.Type() == snap.TypeSnapd && apparmor_sandbox.ProbedLevel() != apparmor_sandbox.Unsupported { 361 if err := b.setupSnapConfineReexec(snapInfo); err != nil { 362 return nil, fmt.Errorf("cannot create host snap-confine apparmor configuration: %s", err) 363 } 364 } 365 366 // core on core devices is also special, the apparmor cache gets 367 // confused too easy, especially at rollbacks, so we delete the cache. 368 // See LP:#1460152 and 369 // https://forum.snapcraft.io/t/core-snap-revert-issues-on-core-devices/ 370 // 371 if (snapInfo.Type() == snap.TypeOS || snapInfo.Type() == snap.TypeSnapd) && !release.OnClassic { 372 if li, err := filepath.Glob(filepath.Join(apparmor_sandbox.SystemCacheDir, "*")); err == nil { 373 for _, p := range li { 374 if st, err := os.Stat(p); err == nil && st.Mode().IsRegular() && profileIsRemovableOnCoreSetup(p) { 375 if err := os.Remove(p); err != nil { 376 logger.Noticef("cannot remove %q: %s", p, err) 377 } 378 } 379 } 380 } 381 } 382 383 // Get the files that this snap should have 384 content, err := b.deriveContent(spec.(*Specification), snapInfo, opts) 385 if err != nil { 386 return nil, fmt.Errorf("cannot obtain expected security files for snap %q: %s", snapName, err) 387 } 388 dir := dirs.SnapAppArmorDir 389 globs := profileGlobs(snapInfo.InstanceName()) 390 if err := os.MkdirAll(dir, 0755); err != nil { 391 return nil, fmt.Errorf("cannot create directory for apparmor profiles %q: %s", dir, err) 392 } 393 changed, removedPaths, errEnsure := osutil.EnsureDirStateGlobs(dir, globs, content) 394 // XXX: in the old code this error was reported late, after doing load/unload. 395 if errEnsure != nil { 396 return nil, fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, errEnsure) 397 } 398 399 // Find the set of unchanged profiles. 400 unchanged := make([]string, 0, len(content)-len(changed)) 401 for name := range content { 402 // changed is pre-sorted by EnsureDirStateGlobs 403 x := sort.SearchStrings(changed, name) 404 if x < len(changed) && changed[x] == name { 405 continue 406 } 407 unchanged = append(unchanged, name) 408 } 409 sort.Strings(unchanged) 410 411 changedPaths := make([]string, len(changed)) 412 for i, profile := range changed { 413 changedPaths[i] = filepath.Join(dir, profile) 414 } 415 416 unchangedPaths := make([]string, len(unchanged)) 417 for i, profile := range unchanged { 418 unchangedPaths[i] = filepath.Join(dir, profile) 419 } 420 421 return &profilePathsResults{changed: changedPaths, removed: removedPaths, unchanged: unchangedPaths}, nil 422 } 423 424 // Setup creates and loads apparmor profiles specific to a given snap. 425 // The snap can be in developer mode to make security violations non-fatal to 426 // the offending application process. 427 // 428 // This method should be called after changing plug, slots, connections between 429 // them or application present in the snap. 430 func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) error { 431 prof, err := b.prepareProfiles(snapInfo, opts, repo) 432 if err != nil { 433 return err 434 } 435 436 // Load all changed profiles with a flag that asks apparmor to skip reading 437 // the cache (since we know those changed for sure). This allows us to 438 // work despite time being wrong (e.g. in the past). For more details see 439 // https://forum.snapcraft.io/t/apparmor-profile-caching/1268/18 440 var errReloadChanged error 441 aaFlags := skipReadCache 442 if b.preseed { 443 aaFlags |= skipKernelLoad 444 } 445 timings.Run(tm, "load-profiles[changed]", fmt.Sprintf("load changed security profiles of snap %q", snapInfo.InstanceName()), func(nesttm timings.Measurer) { 446 errReloadChanged = loadProfiles(prof.changed, apparmor_sandbox.CacheDir, aaFlags) 447 }) 448 449 // Load all unchanged profiles anyway. This ensures those are correct in 450 // the kernel even if the files on disk were not changed. We rely on 451 // apparmor cache to make this performant. 452 var errReloadOther error 453 aaFlags = 0 454 if b.preseed { 455 aaFlags |= skipKernelLoad 456 } 457 timings.Run(tm, "load-profiles[unchanged]", fmt.Sprintf("load unchanged security profiles of snap %q", snapInfo.InstanceName()), func(nesttm timings.Measurer) { 458 errReloadOther = loadProfiles(prof.unchanged, apparmor_sandbox.CacheDir, aaFlags) 459 }) 460 errUnload := unloadProfiles(prof.removed, apparmor_sandbox.CacheDir) 461 if errReloadChanged != nil { 462 return errReloadChanged 463 } 464 if errReloadOther != nil { 465 return errReloadOther 466 } 467 return errUnload 468 } 469 470 // SetupMany creates and loads apparmor profiles for multiple snaps. 471 // The snaps can be in developer mode to make security violations non-fatal to 472 // the offending application process. 473 // SetupMany tries to recreate all profiles without interrupting on errors, but 474 // collects and returns them all. 475 // 476 // This method is useful mainly for regenerating profiles. 477 func (b *Backend) SetupMany(snaps []*snap.Info, confinement func(snapName string) interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) []error { 478 var allChangedPaths, allUnchangedPaths, allRemovedPaths []string 479 var fallback bool 480 for _, snapInfo := range snaps { 481 opts := confinement(snapInfo.InstanceName()) 482 prof, err := b.prepareProfiles(snapInfo, opts, repo) 483 if err != nil { 484 fallback = true 485 break 486 } 487 allChangedPaths = append(allChangedPaths, prof.changed...) 488 allUnchangedPaths = append(allUnchangedPaths, prof.unchanged...) 489 allRemovedPaths = append(allRemovedPaths, prof.removed...) 490 } 491 492 if !fallback { 493 aaFlags := skipReadCache | conserveCPU 494 if b.preseed { 495 aaFlags |= skipKernelLoad 496 } 497 var errReloadChanged error 498 timings.Run(tm, "load-profiles[changed-many]", fmt.Sprintf("load changed security profiles of %d snaps", len(snaps)), func(nesttm timings.Measurer) { 499 errReloadChanged = loadProfiles(allChangedPaths, apparmor_sandbox.CacheDir, aaFlags) 500 }) 501 502 aaFlags = conserveCPU 503 if b.preseed { 504 aaFlags |= skipKernelLoad 505 } 506 var errReloadOther error 507 timings.Run(tm, "load-profiles[unchanged-many]", fmt.Sprintf("load unchanged security profiles %d snaps", len(snaps)), func(nesttm timings.Measurer) { 508 errReloadOther = loadProfiles(allUnchangedPaths, apparmor_sandbox.CacheDir, aaFlags) 509 }) 510 511 errUnload := unloadProfiles(allRemovedPaths, apparmor_sandbox.CacheDir) 512 if errReloadChanged != nil { 513 logger.Noticef("failed to batch-reload changed profiles: %s", errReloadChanged) 514 fallback = true 515 } 516 if errReloadOther != nil { 517 logger.Noticef("failed to batch-reload unchanged profiles: %s", errReloadOther) 518 fallback = true 519 } 520 if errUnload != nil { 521 logger.Noticef("failed to batch-unload profiles: %s", errUnload) 522 fallback = true 523 } 524 } 525 526 var errors []error 527 // if an error was encountered when processing all profiles at once, re-try them one by one 528 if fallback { 529 for _, snapInfo := range snaps { 530 opts := confinement(snapInfo.InstanceName()) 531 if err := b.Setup(snapInfo, opts, repo, tm); err != nil { 532 errors = append(errors, fmt.Errorf("cannot setup profiles for snap %q: %s", snapInfo.InstanceName(), err)) 533 } 534 } 535 } 536 return errors 537 } 538 539 // Remove removes and unloads apparmor profiles of a given snap. 540 func (b *Backend) Remove(snapName string) error { 541 dir := dirs.SnapAppArmorDir 542 globs := profileGlobs(snapName) 543 cache := apparmor_sandbox.CacheDir 544 _, removed, errEnsure := osutil.EnsureDirStateGlobs(dir, globs, nil) 545 errUnload := unloadProfiles(removed, cache) 546 if errEnsure != nil { 547 return fmt.Errorf("cannot synchronize security files for snap %q: %s", snapName, errEnsure) 548 } 549 return errUnload 550 } 551 552 var ( 553 templatePattern = regexp.MustCompile("(###[A-Z_]+###)") 554 coreRuntimePattern = regexp.MustCompile("^core([0-9][0-9])?$") 555 ) 556 557 const ( 558 attachPattern = "(attach_disconnected,mediate_deleted)" 559 attachComplain = "(attach_disconnected,mediate_deleted,complain)" 560 ) 561 562 func (b *Backend) deriveContent(spec *Specification, snapInfo *snap.Info, opts interfaces.ConfinementOptions) (content map[string]osutil.FileState, err error) { 563 content = make(map[string]osutil.FileState, len(snapInfo.Apps)+len(snapInfo.Hooks)+1) 564 565 // Add profile for each app. 566 for _, appInfo := range snapInfo.Apps { 567 securityTag := appInfo.SecurityTag() 568 addContent(securityTag, snapInfo, appInfo.Name, opts, spec.SnippetForTag(securityTag), content, spec) 569 } 570 // Add profile for each hook. 571 for _, hookInfo := range snapInfo.Hooks { 572 securityTag := hookInfo.SecurityTag() 573 addContent(securityTag, snapInfo, "hook."+hookInfo.Name, opts, spec.SnippetForTag(securityTag), content, spec) 574 } 575 // Add profile for snap-update-ns if we have any apps or hooks. 576 // If we have neither then we don't have any need to create an executing environment. 577 // This applies to, for example, kernel snaps or gadget snaps (unless they have hooks). 578 if len(content) > 0 { 579 snippets := strings.Join(spec.UpdateNS(), "\n") 580 addUpdateNSProfile(snapInfo, opts, snippets, content) 581 } 582 583 return content, nil 584 } 585 586 // addUpdateNSProfile adds an apparmor profile for snap-update-ns, tailored to a specific snap. 587 // 588 // This profile exists so that snap-update-ns doens't need to carry very wide, open permissions 589 // that are suitable for poking holes (and writing) in nearly arbitrary places. Instead the profile 590 // contains just the permissions needed to poke a hole and write to the layout-specific paths. 591 func addUpdateNSProfile(snapInfo *snap.Info, opts interfaces.ConfinementOptions, snippets string, content map[string]osutil.FileState) { 592 // Compute the template by injecting special updateNS snippets. 593 policy := templatePattern.ReplaceAllStringFunc(updateNSTemplate, func(placeholder string) string { 594 switch placeholder { 595 case "###SNAP_INSTANCE_NAME###": 596 return snapInfo.InstanceName() 597 case "###SNIPPETS###": 598 if overlayRoot, _ := isRootWritableOverlay(); overlayRoot != "" { 599 snippets += strings.Replace(overlayRootSnippet, "###UPPERDIR###", overlayRoot, -1) 600 } 601 return snippets 602 } 603 return "" 604 }) 605 606 // Ensure that the snap-update-ns profile is on disk. 607 profileName := nsProfile(snapInfo.InstanceName()) 608 content[profileName] = &osutil.MemoryFileState{ 609 Content: []byte(policy), 610 Mode: 0644, 611 } 612 } 613 614 func downgradeConfinement() bool { 615 kver := osutil.KernelVersion() 616 switch { 617 case release.DistroLike("opensuse-tumbleweed"): 618 if cmp, _ := strutil.VersionCompare(kver, "4.16"); cmp >= 0 { 619 // As a special exception, for openSUSE Tumbleweed which ships Linux 620 // 4.16, do not downgrade the confinement template. 621 return false 622 } 623 case release.DistroLike("arch", "archlinux"): 624 // The default kernel has AppArmor enabled since 4.18.8, the 625 // hardened one since 4.17.4 626 return false 627 } 628 return true 629 } 630 631 func addContent(securityTag string, snapInfo *snap.Info, cmdName string, opts interfaces.ConfinementOptions, snippetForTag string, content map[string]osutil.FileState, spec *Specification) { 632 // If base is specified and it doesn't match the core snaps (not 633 // specifying a base should use the default core policy since in this 634 // case, the 'core' snap is used for the runtime), use the base 635 // apparmor template, otherwise use the default template. 636 var policy string 637 if snapInfo.Base != "" && !coreRuntimePattern.MatchString(snapInfo.Base) { 638 policy = defaultOtherBaseTemplate 639 } else { 640 policy = defaultCoreRuntimeTemplate 641 } 642 643 ignoreSnippets := false 644 // Classic confinement (unless overridden by JailMode) has a dedicated 645 // permissive template that applies a strict, but very open, policy. 646 if opts.Classic && !opts.JailMode { 647 policy = classicTemplate 648 ignoreSnippets = true 649 } 650 // When partial AppArmor is detected, use the classic template for now. We could 651 // use devmode, but that could generate confusing log entries for users running 652 // snaps on systems with partial AppArmor support. 653 if apparmor_sandbox.ProbedLevel() == apparmor_sandbox.Partial { 654 // By default, downgrade confinement to the classic template when 655 // partial AppArmor support is detected. We don't want to use strict 656 // in general yet because older versions of the kernel did not 657 // provide backwards compatible interpretation of confinement 658 // so the meaning of the template would change across kernel 659 // versions and we have not validated that the current template 660 // is operational on older kernels. 661 if downgradeConfinement() { 662 policy = classicTemplate 663 ignoreSnippets = true 664 } 665 } 666 // If a snap is in devmode (or is using classic confinement) then make the 667 // profile non-enforcing where violations are logged but not denied. 668 // This is also done for classic so that no confinement applies. Just in 669 // case the profile we start with is not permissive enough. 670 if (opts.DevMode || opts.Classic) && !opts.JailMode { 671 policy = strings.Replace(policy, attachPattern, attachComplain, -1) 672 } 673 policy = templatePattern.ReplaceAllStringFunc(policy, func(placeholder string) string { 674 switch placeholder { 675 case "###VAR###": 676 return templateVariables(snapInfo, securityTag, cmdName) 677 case "###PROFILEATTACH###": 678 return fmt.Sprintf("profile \"%s\"", securityTag) 679 case "###CHANGEPROFILE_RULE###": 680 features, _ := parserFeatures() 681 for _, f := range features { 682 if f == "unsafe" { 683 return "change_profile unsafe /**," 684 } 685 } 686 return "change_profile," 687 case "###SNIPPETS###": 688 var tagSnippets string 689 if opts.Classic && opts.JailMode { 690 // Add a special internal snippet for snaps using classic confinement 691 // and jailmode together. This snippet provides access to the core snap 692 // so that the dynamic linker and shared libraries can be used. 693 tagSnippets = classicJailmodeSnippet + "\n" + snippetForTag 694 } else if ignoreSnippets { 695 // When classic confinement template is in effect we are 696 // ignoring all apparmor snippets as they may conflict with the 697 // super-broad template we are starting with. 698 } else { 699 // Check if NFS is mounted at or under $HOME. Because NFS is not 700 // transparent to apparmor we must alter the profile to counter that and 701 // allow access to SNAP_USER_* files. 702 tagSnippets = snippetForTag 703 if nfs, _ := isHomeUsingNFS(); nfs { 704 tagSnippets += nfsSnippet 705 } 706 707 if overlayRoot, _ := isRootWritableOverlay(); overlayRoot != "" { 708 snippet := strings.Replace(overlayRootSnippet, "###UPPERDIR###", overlayRoot, -1) 709 tagSnippets += snippet 710 } 711 } 712 713 if !ignoreSnippets { 714 // For policy with snippets that request 715 // suppression of 'ptrace (trace)' denials, add 716 // the suppression rule unless another 717 // interface said it uses them. 718 if spec.SuppressPtraceTrace() && !spec.UsesPtraceTrace() { 719 tagSnippets += ptraceTraceDenySnippet 720 } 721 722 // Use 'ix' rules in the home interface unless an 723 // interface asked to suppress them 724 repl := "ix" 725 if spec.SuppressHomeIx() { 726 repl = "" 727 } 728 tagSnippets = strings.Replace(tagSnippets, "###HOME_IX###", repl, -1) 729 730 // Conditionally add privilege dropping policy 731 if len(snapInfo.SystemUsernames) > 0 { 732 tagSnippets += privDropAndChownRules 733 } 734 } 735 736 return tagSnippets 737 } 738 return "" 739 }) 740 741 content[securityTag] = &osutil.MemoryFileState{ 742 Content: []byte(policy), 743 Mode: 0644, 744 } 745 } 746 747 // NewSpecification returns a new, empty apparmor specification. 748 func (b *Backend) NewSpecification() interfaces.Specification { 749 return &Specification{} 750 } 751 752 // SandboxFeatures returns the list of apparmor features supported by the kernel. 753 func (b *Backend) SandboxFeatures() []string { 754 if apparmor_sandbox.ProbedLevel() == apparmor_sandbox.Unsupported { 755 return nil 756 } 757 758 kFeatures, _ := kernelFeatures() 759 pFeatures, _ := parserFeatures() 760 tags := make([]string, 0, len(kFeatures)+len(pFeatures)) 761 for _, feature := range kFeatures { 762 // Prepend "kernel:" to apparmor kernel features to namespace them and 763 // allow us to introduce our own tags later. 764 tags = append(tags, "kernel:"+feature) 765 } 766 767 for _, feature := range pFeatures { 768 // Prepend "parser:" to apparmor kernel features to namespace 769 // them and allow us to introduce our own tags later. 770 tags = append(tags, "parser:"+feature) 771 } 772 773 level := "full" 774 policy := "default" 775 if apparmor_sandbox.ProbedLevel() == apparmor_sandbox.Partial { 776 level = "partial" 777 778 if downgradeConfinement() { 779 policy = "downgraded" 780 } 781 } 782 tags = append(tags, fmt.Sprintf("support-level:%s", level)) 783 tags = append(tags, fmt.Sprintf("policy:%s", policy)) 784 785 return tags 786 } 787 788 // MockIsHomeUsingNFS mocks the real implementation of osutil.IsHomeUsingNFS. 789 // This is exported so that other packages that indirectly interact with AppArmor backend 790 // can mock isHomeUsingNFS. 791 func MockIsHomeUsingNFS(new func() (bool, error)) (restore func()) { 792 old := isHomeUsingNFS 793 isHomeUsingNFS = new 794 return func() { 795 isHomeUsingNFS = old 796 } 797 }