gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/sysconfig/cloudinit.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 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 sysconfig 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "os/exec" 28 "path/filepath" 29 "regexp" 30 "sort" 31 "strings" 32 33 yaml "gopkg.in/yaml.v2" 34 35 "github.com/snapcore/snapd/asserts" 36 "github.com/snapcore/snapd/dirs" 37 "github.com/snapcore/snapd/logger" 38 "github.com/snapcore/snapd/osutil" 39 "github.com/snapcore/snapd/strutil" 40 ) 41 42 // HasGadgetCloudConf takes a gadget directory and returns whether there is 43 // cloud-init config in the form of a cloud.conf file in the gadget. 44 func HasGadgetCloudConf(gadgetDir string) bool { 45 return osutil.FileExists(filepath.Join(gadgetDir, "cloud.conf")) 46 } 47 48 func ubuntuDataCloudDir(rootdir string) string { 49 return filepath.Join(rootdir, "etc/cloud/") 50 } 51 52 // DisableCloudInit will disable cloud-init permanently by writing a 53 // cloud-init.disabled config file in etc/cloud under the target dir, which 54 // instructs cloud-init-generator to not trigger new cloud-init invocations. 55 // Note that even with this disabled file, a root user could still manually run 56 // cloud-init, but this capability is not provided to any strictly confined 57 // snap. 58 func DisableCloudInit(rootDir string) error { 59 ubuntuDataCloud := ubuntuDataCloudDir(rootDir) 60 if err := os.MkdirAll(ubuntuDataCloud, 0755); err != nil { 61 return fmt.Errorf("cannot make cloud config dir: %v", err) 62 } 63 if err := ioutil.WriteFile(filepath.Join(ubuntuDataCloud, "cloud-init.disabled"), nil, 0644); err != nil { 64 return fmt.Errorf("cannot disable cloud-init: %v", err) 65 } 66 67 return nil 68 } 69 70 // supportedFilteredCloudConfig is a struct of the supported values for 71 // cloud-init configuration file. 72 type supportedFilteredCloudConfig struct { 73 Datasource map[string]supportedFilteredDatasource `yaml:"datasource,omitempty"` 74 Network map[string]interface{} `yaml:"network,omitempty"` 75 // DatasourceList is a pointer so we can distinguish between: 76 // datasource_list: [] 77 // and not setting the datasource at all 78 // for example there might be gadgets which don't want to use any 79 // datasources, but still wants to set some networking config 80 DatasourceList *[]string `yaml:"datasource_list,omitempty"` 81 Reporting map[string]supportedFilteredReporting `yaml:"reporting,omitempty"` 82 } 83 84 type supportedFilteredDatasource struct { 85 // these are for MAAS 86 ConsumerKey string `yaml:"consumer_key,omitempty"` 87 MetadataURL string `yaml:"metadata_url,omitempty"` 88 TokenKey string `yaml:"token_key,omitempty"` 89 TokenSecret string `yaml:"token_secret,omitempty"` 90 } 91 92 type supportedFilteredReporting struct { 93 Type string `yaml:"type,omitempty"` 94 Endpoint string `yaml:"endpoint,omitempty"` 95 ConsumerKey string `yaml:"consumer_key,omitempty"` 96 TokenKey string `yaml:"token_key,omitempty"` 97 TokenSecret string `yaml:"token_secret,omitempty"` 98 } 99 100 // filterCloudCfg filters a cloud-init configuration struct parsed from a single 101 // cloud-init configuration file. The config provided here may be a subset of 102 // the full cloud-init configuration from the file in that there may be 103 // top-level keys in the YAML file that we did not parse and as such they are 104 // dropped and filtered automatically. For other keys, we must parse part of the 105 // configuration struct and remove nested keys while keeping other parts of the 106 // same section. 107 func filterCloudCfg(cfg *supportedFilteredCloudConfig, allowedDatasources []string) error { 108 // TODO: should we track modifications / filters applied to log/notify about 109 // what is dropped / not supported? 110 111 // first filter out the disallowed datasources 112 for dsName := range cfg.Datasource { 113 // remove unsupported or unrecognized datasources 114 if !strutil.ListContains(allowedDatasources, strings.ToUpper(dsName)) { 115 delete(cfg.Datasource, dsName) 116 continue 117 } 118 } 119 120 // next handle the datasource list setting, if it was not empty, reset it to 121 // the allowedDatasources we were provided 122 if cfg.DatasourceList != nil { 123 deepCpy := make([]string, 0, len(allowedDatasources)) 124 deepCpy = append(deepCpy, allowedDatasources...) 125 cfg.DatasourceList = &deepCpy 126 } 127 128 // next handle the reporting setting 129 for dsName := range cfg.Reporting { 130 // remove unsupported or unrecognized datasources 131 if !strutil.ListContains(allowedDatasources, strings.ToUpper(dsName)) { 132 delete(cfg.Reporting, dsName) 133 continue 134 } 135 } 136 137 return nil 138 } 139 140 // filterCloudCfgFile takes a cloud config file as input and filters out unknown 141 // and unsupported keys from the config, returning a new file. It also will 142 // filter out configuration that is specific to a datasource if that datasource 143 // is not specified in the allowedDatasources argument. The empty string will be 144 // returned if the input file was entirely filtered out and there is nothing 145 // left. 146 func filterCloudCfgFile(in string, allowedDatasources []string) (string, error) { 147 dstFileName := filepath.Base(in) 148 filteredFile, err := ioutil.TempFile("", dstFileName) 149 if err != nil { 150 return "", err 151 } 152 defer filteredFile.Close() 153 154 // open the source and unmarshal it as yaml 155 unfilteredFileBytes, err := ioutil.ReadFile(in) 156 if err != nil { 157 return "", err 158 } 159 160 var cfg supportedFilteredCloudConfig 161 if err := yaml.Unmarshal(unfilteredFileBytes, &cfg); err != nil { 162 return "", err 163 } 164 165 if err := filterCloudCfg(&cfg, allowedDatasources); err != nil { 166 return "", err 167 } 168 169 // write out cfg to the filtered file now 170 b, err := yaml.Marshal(cfg) 171 if err != nil { 172 return "", err 173 } 174 175 // check if we need to write a file at all, if the yaml serialization was 176 // entirely filtered out, then we don't need to write anything 177 if strings.TrimSpace(string(b)) == "{}" { 178 return "", nil 179 } 180 181 // add the #cloud-config prefix to all files we write 182 if _, err := filteredFile.Write([]byte("#cloud-config\n")); err != nil { 183 return "", err 184 } 185 186 if _, err := filteredFile.Write(b); err != nil { 187 return "", err 188 } 189 190 // use the newly filtered temp file as the source to copy 191 return filteredFile.Name(), nil 192 } 193 194 type cloudDatasourcesInUseResult struct { 195 // ExplicitlyAllowed is the value of datasource_list. If this is empty, 196 // consult ExplicitlyNoneAllowed to tell if it was specified as empty in the 197 // config or if it was just absent from the config 198 ExplicitlyAllowed []string 199 // ExplicitlyNoneAllowed is true when datasource_list was set to 200 // specifically the empty list, thus disallowing use of any datasource 201 ExplicitlyNoneAllowed bool 202 // Mentioned is the full set of datasources mentioned in the yaml config. 203 Mentioned []string 204 } 205 206 // cloudDatasourcesInUse returns the datasources in use by the specified config 207 // file. All datasource names are made upper case to be comparable. This is an 208 // arbitrary choice between making them upper case or making them lower case, 209 // but cloud-init treats "maas" the same as "MAAS", so we need to treat them the 210 // same too. 211 func cloudDatasourcesInUse(configFile string) (*cloudDatasourcesInUseResult, error) { 212 // TODO: are there other keys in addition to those that we support in 213 // filtering that might mention datasources ? 214 215 b, err := ioutil.ReadFile(configFile) 216 if err != nil { 217 return nil, err 218 } 219 220 var cfg supportedFilteredCloudConfig 221 if err := yaml.Unmarshal(b, &cfg); err != nil { 222 return nil, err 223 } 224 225 res := &cloudDatasourcesInUseResult{} 226 227 sourcesMentionedInCfg := map[string]bool{} 228 229 // datasource key is a map with the datasource name as a key 230 for ds := range cfg.Datasource { 231 sourcesMentionedInCfg[strings.ToUpper(ds)] = true 232 } 233 234 // same for reporting 235 for ds := range cfg.Reporting { 236 sourcesMentionedInCfg[strings.ToUpper(ds)] = true 237 } 238 239 // we can also have datasources mentioned in the datasource list config 240 if cfg.DatasourceList != nil { 241 if len(*cfg.DatasourceList) == 0 { 242 res.ExplicitlyNoneAllowed = true 243 } else { 244 explicitlyAllowed := map[string]bool{} 245 for _, ds := range *cfg.DatasourceList { 246 dsName := strings.ToUpper(ds) 247 sourcesMentionedInCfg[dsName] = true 248 explicitlyAllowed[dsName] = true 249 } 250 res.ExplicitlyAllowed = make([]string, 0, len(explicitlyAllowed)) 251 for ds := range explicitlyAllowed { 252 res.ExplicitlyAllowed = append(res.ExplicitlyAllowed, ds) 253 } 254 sort.Strings(res.ExplicitlyAllowed) 255 } 256 } 257 258 for ds := range sourcesMentionedInCfg { 259 res.Mentioned = append(res.Mentioned, strings.ToUpper(ds)) 260 } 261 sort.Strings(res.Mentioned) 262 263 return res, nil 264 } 265 266 type cloudInitConfigInstallOptions struct { 267 // Prefix is the prefix to add to files when installing them. 268 Prefix string 269 // Filter is whether to filter the config files when installing them. 270 Filter bool 271 // AllowedDatasources is the set of datasources to allow config that is 272 // specific to a datasource in when filtering. An empty list and setting 273 // Filter to false is equivalent to allowing any datasource to be installed, 274 // while an empty list and setting Filter to true means that no config that 275 // is specific to a datasource should be installed, but config that is not 276 // specific to a datasource (such as networking config) is allowed to be 277 // installed. 278 AllowedDatasources []string 279 } 280 281 // installCloudInitCfgDir installs glob cfg files from the source directory to 282 // the cloud config dir, optionally filtering the files for safe and supported 283 // keys in the configuration before installing them. 284 func installCloudInitCfgDir(src, targetdir string, opts *cloudInitConfigInstallOptions) error { 285 if opts == nil { 286 opts = &cloudInitConfigInstallOptions{} 287 } 288 289 // TODO:UC20: enforce patterns on the glob files and their suffix ranges 290 ccl, err := filepath.Glob(filepath.Join(src, "*.cfg")) 291 if err != nil { 292 return err 293 } 294 if len(ccl) == 0 { 295 return nil 296 } 297 298 ubuntuDataCloudCfgDir := filepath.Join(ubuntuDataCloudDir(targetdir), "cloud.cfg.d/") 299 if err := os.MkdirAll(ubuntuDataCloudCfgDir, 0755); err != nil { 300 return fmt.Errorf("cannot make cloud config dir: %v", err) 301 } 302 303 for _, cc := range ccl { 304 if err := osutil.CopyFile(cc, filepath.Join(ubuntuDataCloudCfgDir, opts.Prefix+filepath.Base(cc)), 0); err != nil { 305 return err 306 } 307 } 308 return nil 309 } 310 311 // installGadgetCloudInitCfg installs a single cloud-init config file from the 312 // gadget snap to the /etc/cloud config dir as "80_device_gadget.cfg". It also 313 // parses and returns what datasources are detected to be in use for the gadget 314 // cloud-config. 315 func installGadgetCloudInitCfg(src, targetdir string) (*cloudDatasourcesInUseResult, error) { 316 ubuntuDataCloudCfgDir := filepath.Join(ubuntuDataCloudDir(targetdir), "cloud.cfg.d/") 317 if err := os.MkdirAll(ubuntuDataCloudCfgDir, 0755); err != nil { 318 return nil, fmt.Errorf("cannot make cloud config dir: %v", err) 319 } 320 321 datasourcesRes, err := cloudDatasourcesInUse(src) 322 if err != nil { 323 return nil, err 324 } 325 326 configFile := filepath.Join(ubuntuDataCloudCfgDir, "80_device_gadget.cfg") 327 if err := osutil.CopyFile(src, configFile, 0); err != nil { 328 return nil, err 329 } 330 return datasourcesRes, nil 331 } 332 333 func configureCloudInit(model *asserts.Model, opts *Options) (err error) { 334 if opts.TargetRootDir == "" { 335 return fmt.Errorf("unable to configure cloud-init, missing target dir") 336 } 337 338 // first check if cloud-init should be disallowed entirely 339 if !opts.AllowCloudInit { 340 return DisableCloudInit(WritableDefaultsDir(opts.TargetRootDir)) 341 } 342 343 // otherwise cloud-init is allowed to run, we need to decide where to 344 // permit configuration to come from, if opts.CloudInitSrcDir is non-empty 345 // there is at least a cloud-config dir on ubuntu-seed we could install 346 // config from 347 348 // check if we should filter cloud-init config on ubuntu-seed, we do this 349 // for grade signed only (we don't allow any config for grade secured, and we 350 // allow any config on grade dangerous) 351 352 grade := model.Grade() 353 354 // we always allow gadget cloud config, so install that first 355 if HasGadgetCloudConf(opts.GadgetDir) { 356 // then copy / install the gadget config first 357 gadgetCloudConf := filepath.Join(opts.GadgetDir, "cloud.conf") 358 359 // TODO: save the gadget datasource and use it below in deciding what to 360 // allow through for grade: signed 361 if _, err := installGadgetCloudInitCfg(gadgetCloudConf, WritableDefaultsDir(opts.TargetRootDir)); err != nil { 362 return err 363 } 364 365 // we don't return here to enable also copying any cloud-init config 366 // from ubuntu-seed in order for both to be used simultaneously for 367 // example on test devices where the gadget has a gadget.yaml, but for 368 // testing purposes you also want to provision another user with 369 // ubuntu-seed cloud-init config 370 } 371 372 installOpts := &cloudInitConfigInstallOptions{ 373 // set the prefix such that any ubuntu-seed config that ends up getting 374 // installed takes precedence over the gadget config 375 Prefix: "90_", 376 } 377 378 switch grade { 379 case asserts.ModelSecured: 380 // for secured we are done, we only allow gadget cloud-config on secured 381 return nil 382 case asserts.ModelSigned: 383 // TODO: for grade signed, we will install ubuntu-seed config but filter 384 // it and ensure that the ubuntu-seed config matches the config from the 385 // gadget if that exists 386 // for now though, just return 387 return nil 388 case asserts.ModelDangerous: 389 // for grade dangerous we just install all the config from ubuntu-seed 390 installOpts.Filter = false 391 default: 392 return fmt.Errorf("internal error: unknown model assertion grade %s", grade) 393 } 394 395 if opts.CloudInitSrcDir != "" { 396 return installCloudInitCfgDir(opts.CloudInitSrcDir, WritableDefaultsDir(opts.TargetRootDir), installOpts) 397 } 398 399 // it's valid to allow cloud-init, but not set CloudInitSrcDir and not have 400 // a gadget cloud.conf, in this case cloud-init may pick up dynamic metadata 401 // and userdata from NoCloud sources such as a CD-ROM drive with label 402 // CIDATA, etc. during first-boot 403 404 return nil 405 } 406 407 // CloudInitState represents the various cloud-init states 408 type CloudInitState int 409 410 var ( 411 // the (?m) is needed since cloud-init output will have newlines 412 cloudInitStatusRe = regexp.MustCompile(`(?m)^status: (.*)$`) 413 datasourceRe = regexp.MustCompile(`DataSource([a-zA-Z0-9]+).*`) 414 415 cloudInitSnapdRestrictFile = "/etc/cloud/cloud.cfg.d/zzzz_snapd.cfg" 416 cloudInitDisabledFile = "/etc/cloud/cloud-init.disabled" 417 418 // for NoCloud datasource, we need to specify "manual_cache_clean: true" 419 // because the default is false, and this key being true essentially informs 420 // cloud-init that it should always trust the instance-id it has cached in 421 // the image, and shouldn't assume that there is a new one on every boot, as 422 // otherwise we have bugs like https://bugs.launchpad.net/snapd/+bug/1905983 423 // where subsequent boots after cloud-init runs and gets restricted it will 424 // try to detect the instance_id by reading from the NoCloud datasource 425 // fs_label, but we set that to "null" so it fails to read anything and thus 426 // can't detect the effective instance_id and assumes it is different and 427 // applies default config which can overwrite valid config from the initial 428 // boot if that is not the default config 429 // see also https://cloudinit.readthedocs.io/en/latest/topics/boot.html?highlight=manual_cache_clean#first-boot-determination 430 nocloudRestrictYaml = []byte(`datasource_list: [NoCloud] 431 datasource: 432 NoCloud: 433 fs_label: null 434 manual_cache_clean: true 435 `) 436 437 // don't use manual_cache_clean for real cloud datasources, the setting is 438 // used with ubuntu core only for sources where we can only get the 439 // instance_id through the fs_label for NoCloud and None (since we disable 440 // importing using the fs_label after the initial run). 441 genericCloudRestrictYamlPattern = `datasource_list: [%s] 442 ` 443 444 localDatasources = []string{"NoCloud", "None"} 445 ) 446 447 const ( 448 // CloudInitDisabledPermanently is when cloud-init is disabled as per the 449 // cloud-init.disabled file. 450 CloudInitDisabledPermanently CloudInitState = iota 451 // CloudInitRestrictedBySnapd is when cloud-init has been restricted by 452 // snapd with a specific config file. 453 CloudInitRestrictedBySnapd 454 // CloudInitUntriggered is when cloud-init is disabled because nothing has 455 // triggered it to run, but it could still be run. 456 CloudInitUntriggered 457 // CloudInitDone is when cloud-init has been run on this boot. 458 CloudInitDone 459 // CloudInitEnabled is when cloud-init is active, but not necessarily 460 // finished. This matches the "running" and "not run" states from cloud-init 461 // as well as any other state that does not match any of the other defined 462 // states, as we are conservative in assuming that cloud-init is doing 463 // something. 464 CloudInitEnabled 465 // CloudInitNotFound is when there is no cloud-init executable on the 466 // device. 467 CloudInitNotFound 468 // CloudInitErrored is when cloud-init tried to run, but failed or had invalid 469 // configuration. 470 CloudInitErrored 471 ) 472 473 // CloudInitStatus returns the current status of cloud-init. Note that it will 474 // first check for static file-based statuses first through the snapd 475 // restriction file and the disabled file before consulting 476 // cloud-init directly through the status command. 477 // Also note that in unknown situations we are conservative in assuming that 478 // cloud-init may be doing something and will return CloudInitEnabled when we 479 // do not recognize the state returned by the cloud-init status command. 480 func CloudInitStatus() (CloudInitState, error) { 481 // if cloud-init has been restricted by snapd, check that first 482 snapdRestrictingFile := filepath.Join(dirs.GlobalRootDir, cloudInitSnapdRestrictFile) 483 if osutil.FileExists(snapdRestrictingFile) { 484 return CloudInitRestrictedBySnapd, nil 485 } 486 487 // if it was explicitly disabled via the cloud-init disable file, then 488 // return special status for that 489 disabledFile := filepath.Join(dirs.GlobalRootDir, cloudInitDisabledFile) 490 if osutil.FileExists(disabledFile) { 491 return CloudInitDisabledPermanently, nil 492 } 493 494 ciBinary, err := exec.LookPath("cloud-init") 495 if err != nil { 496 logger.Noticef("cannot locate cloud-init executable: %v", err) 497 return CloudInitNotFound, nil 498 } 499 500 out, err := exec.Command(ciBinary, "status").CombinedOutput() 501 502 // in the case where cloud-init is actually in an error condition, like 503 // where MAAS is the datasource but there is no MAAS server for example, 504 // then cloud-init will exit with status 1 and output `status: error` 505 // we want to handle that case specially below by returning non-nil error, 506 // but also CloudInitErrored, so first inspect the output to see if it 507 // matches 508 // output should just be "status: <state>" 509 match := cloudInitStatusRe.FindSubmatch(out) 510 if len(match) != 2 { 511 // check if running the command had an error, if it did then return that 512 if err != nil { 513 return CloudInitErrored, osutil.OutputErr(out, err) 514 } 515 // otherwise we had some sort of malformed output 516 return CloudInitErrored, fmt.Errorf("invalid cloud-init output: %v", osutil.OutputErr(out, err)) 517 } 518 519 // otherwise we had a successful match, but we need to check if the status 520 // command errored itself 521 if err != nil { 522 if string(match[1]) == "error" { 523 // then the status was indeed error and we should treat this as the 524 // "positively identified" error case 525 return CloudInitErrored, nil 526 } 527 // otherwise just ignore the parsing of the output and just return the 528 // error normally 529 return CloudInitErrored, fmt.Errorf("cloud-init errored: %v", osutil.OutputErr(out, err)) 530 } 531 532 // otherwise no error from cloud-init 533 534 switch string(match[1]) { 535 case "disabled": 536 // here since we weren't disabled by the file, we are in "disabled but 537 // could be enabled" state - arguably this should be a different state 538 // than "disabled", see 539 // https://bugs.launchpad.net/cloud-init/+bug/1883124 and 540 // https://bugs.launchpad.net/cloud-init/+bug/1883122 541 return CloudInitUntriggered, nil 542 case "error": 543 // this shouldn't happen in practice, but handle it here anyways in case 544 // cloud-init ever changes it's mind and starts reporting error state 545 // with a 0 exit code 546 return CloudInitErrored, nil 547 case "done": 548 return CloudInitDone, nil 549 // "running" and "not run" are considered Enabled, see doc-comment 550 case "running", "not run": 551 fallthrough 552 default: 553 // these states are all the generic "enabled" state 554 return CloudInitEnabled, nil 555 } 556 } 557 558 // these structs are externally defined by cloud-init 559 type v1Data struct { 560 DataSource string `json:"datasource"` 561 } 562 563 type cloudInitStatus struct { 564 V1 v1Data `json:"v1"` 565 } 566 567 // CloudInitRestrictionResult is the result of calling RestrictCloudInit. The 568 // values for Action are "disable" or "restrict", and the Datasource will be set 569 // to the restricted datasource if Action is "restrict". 570 type CloudInitRestrictionResult struct { 571 Action string 572 DataSource string 573 } 574 575 // CloudInitRestrictOptions are options for how to restrict cloud-init with 576 // RestrictCloudInit. 577 type CloudInitRestrictOptions struct { 578 // ForceDisable will force disabling cloud-init even if it is 579 // in an active/running or errored state. 580 ForceDisable bool 581 582 // DisableAfterLocalDatasourcesRun modifies RestrictCloudInit to disable 583 // cloud-init after it has run on first-boot if the datasource detected is 584 // a local source such as NoCloud or None. If the datasource detected is not 585 // a local source, such as GCE or AWS EC2 it is merely restricted as 586 // described in the doc-comment on RestrictCloudInit. 587 DisableAfterLocalDatasourcesRun bool 588 } 589 590 // RestrictCloudInit will limit the operations of cloud-init on subsequent boots 591 // by either disabling cloud-init in the untriggered state, or restrict 592 // cloud-init to only use a specific datasource (additionally if the currently 593 // detected datasource for this boot was NoCloud, it will disable the automatic 594 // import of filesystems with labels such as CIDATA (or cidata) as datasources). 595 // This is expected to be run when cloud-init is in a "steady" state such as 596 // done or disabled (untriggered). If called in other states such as errored, it 597 // will return an error, but it can be forced to disable cloud-init anyways in 598 // these states with the opts parameter and the ForceDisable field. 599 // This function is meant to protect against CVE-2020-11933. 600 func RestrictCloudInit(state CloudInitState, opts *CloudInitRestrictOptions) (CloudInitRestrictionResult, error) { 601 res := CloudInitRestrictionResult{} 602 603 if opts == nil { 604 opts = &CloudInitRestrictOptions{} 605 } 606 607 switch state { 608 case CloudInitDone: 609 // handled below 610 break 611 case CloudInitRestrictedBySnapd: 612 return res, fmt.Errorf("cannot restrict cloud-init: already restricted") 613 case CloudInitDisabledPermanently: 614 return res, fmt.Errorf("cannot restrict cloud-init: already disabled") 615 case CloudInitErrored, CloudInitEnabled: 616 // if we are not forcing a disable, return error as these states are 617 // where cloud-init could still be running doing things 618 if !opts.ForceDisable { 619 return res, fmt.Errorf("cannot restrict cloud-init in error or enabled state") 620 } 621 fallthrough 622 case CloudInitUntriggered, CloudInitNotFound: 623 fallthrough 624 default: 625 res.Action = "disable" 626 return res, DisableCloudInit(dirs.GlobalRootDir) 627 } 628 629 // from here on out, we are taking the "restrict" action 630 res.Action = "restrict" 631 632 // first get the cloud-init data-source that was used from / 633 resultsFile := filepath.Join(dirs.GlobalRootDir, "/run/cloud-init/status.json") 634 635 f, err := os.Open(resultsFile) 636 if err != nil { 637 return res, err 638 } 639 defer f.Close() 640 641 var stat cloudInitStatus 642 err = json.NewDecoder(f).Decode(&stat) 643 if err != nil { 644 return res, err 645 } 646 647 // if the datasource was empty then cloud-init did something wrong or 648 // perhaps it incorrectly reported that it ran but something else deleted 649 // the file 650 datasourceRaw := stat.V1.DataSource 651 if datasourceRaw == "" { 652 return res, fmt.Errorf("cloud-init error: missing datasource from status.json") 653 } 654 655 // for some datasources there is additional data in this item, i.e. for 656 // NoCloud we will also see: 657 // "DataSourceNoCloud [seed=/dev/sr0][dsmode=net]" 658 // so hence we use a regexp to parse out just the name of the datasource 659 datasourceMatches := datasourceRe.FindStringSubmatch(datasourceRaw) 660 if len(datasourceMatches) != 2 { 661 return res, fmt.Errorf("cloud-init error: unexpected datasource format %q", datasourceRaw) 662 } 663 res.DataSource = datasourceMatches[1] 664 665 cloudInitRestrictFile := filepath.Join(dirs.GlobalRootDir, cloudInitSnapdRestrictFile) 666 667 switch { 668 case opts.DisableAfterLocalDatasourcesRun && strutil.ListContains(localDatasources, res.DataSource): 669 // On UC20, DisableAfterLocalDatasourcesRun will be set, where we want 670 // to disable local sources like NoCloud and None after first-boot 671 // instead of just restricting them like we do below for UC16 and UC18. 672 673 // as such, change the action taken to disable and disable cloud-init 674 res.Action = "disable" 675 err = DisableCloudInit(dirs.GlobalRootDir) 676 case res.DataSource == "NoCloud": 677 // With the NoCloud datasource (which is one of the local datasources), 678 // we also need to restrict/disable the import of arbitrary filesystem 679 // labels to use as datasources, i.e. a USB drive inserted by an 680 // attacker with label CIDATA will defeat security measures on Ubuntu 681 // Core, so with the additional fs_label spec, we disable that import. 682 err = ioutil.WriteFile(cloudInitRestrictFile, nocloudRestrictYaml, 0644) 683 default: 684 // all other cases are either not local on UC20, or not NoCloud and as 685 // such we simply restrict cloud-init to the specific datasource used so 686 // that an attack via NoCloud is protected against 687 yaml := []byte(fmt.Sprintf(genericCloudRestrictYamlPattern, res.DataSource)) 688 err = ioutil.WriteFile(cloudInitRestrictFile, yaml, 0644) 689 } 690 691 return res, err 692 }