github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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 if err != nil { 502 return CloudInitErrored, osutil.OutputErr(out, err) 503 } 504 // output should just be "status: <state>" 505 match := cloudInitStatusRe.FindSubmatch(out) 506 if len(match) != 2 { 507 return CloudInitErrored, fmt.Errorf("invalid cloud-init output: %v", osutil.OutputErr(out, err)) 508 } 509 switch string(match[1]) { 510 case "disabled": 511 // here since we weren't disabled by the file, we are in "disabled but 512 // could be enabled" state - arguably this should be a different state 513 // than "disabled", see 514 // https://bugs.launchpad.net/cloud-init/+bug/1883124 and 515 // https://bugs.launchpad.net/cloud-init/+bug/1883122 516 return CloudInitUntriggered, nil 517 case "error": 518 return CloudInitErrored, nil 519 case "done": 520 return CloudInitDone, nil 521 // "running" and "not run" are considered Enabled, see doc-comment 522 case "running", "not run": 523 fallthrough 524 default: 525 // these states are all 526 return CloudInitEnabled, nil 527 } 528 } 529 530 // these structs are externally defined by cloud-init 531 type v1Data struct { 532 DataSource string `json:"datasource"` 533 } 534 535 type cloudInitStatus struct { 536 V1 v1Data `json:"v1"` 537 } 538 539 // CloudInitRestrictionResult is the result of calling RestrictCloudInit. The 540 // values for Action are "disable" or "restrict", and the Datasource will be set 541 // to the restricted datasource if Action is "restrict". 542 type CloudInitRestrictionResult struct { 543 Action string 544 DataSource string 545 } 546 547 // CloudInitRestrictOptions are options for how to restrict cloud-init with 548 // RestrictCloudInit. 549 type CloudInitRestrictOptions struct { 550 // ForceDisable will force disabling cloud-init even if it is 551 // in an active/running or errored state. 552 ForceDisable bool 553 554 // DisableAfterLocalDatasourcesRun modifies RestrictCloudInit to disable 555 // cloud-init after it has run on first-boot if the datasource detected is 556 // a local source such as NoCloud or None. If the datasource detected is not 557 // a local source, such as GCE or AWS EC2 it is merely restricted as 558 // described in the doc-comment on RestrictCloudInit. 559 DisableAfterLocalDatasourcesRun bool 560 } 561 562 // RestrictCloudInit will limit the operations of cloud-init on subsequent boots 563 // by either disabling cloud-init in the untriggered state, or restrict 564 // cloud-init to only use a specific datasource (additionally if the currently 565 // detected datasource for this boot was NoCloud, it will disable the automatic 566 // import of filesystems with labels such as CIDATA (or cidata) as datasources). 567 // This is expected to be run when cloud-init is in a "steady" state such as 568 // done or disabled (untriggered). If called in other states such as errored, it 569 // will return an error, but it can be forced to disable cloud-init anyways in 570 // these states with the opts parameter and the ForceDisable field. 571 // This function is meant to protect against CVE-2020-11933. 572 func RestrictCloudInit(state CloudInitState, opts *CloudInitRestrictOptions) (CloudInitRestrictionResult, error) { 573 res := CloudInitRestrictionResult{} 574 575 if opts == nil { 576 opts = &CloudInitRestrictOptions{} 577 } 578 579 switch state { 580 case CloudInitDone: 581 // handled below 582 break 583 case CloudInitRestrictedBySnapd: 584 return res, fmt.Errorf("cannot restrict cloud-init: already restricted") 585 case CloudInitDisabledPermanently: 586 return res, fmt.Errorf("cannot restrict cloud-init: already disabled") 587 case CloudInitErrored, CloudInitEnabled: 588 // if we are not forcing a disable, return error as these states are 589 // where cloud-init could still be running doing things 590 if !opts.ForceDisable { 591 return res, fmt.Errorf("cannot restrict cloud-init in error or enabled state") 592 } 593 fallthrough 594 case CloudInitUntriggered, CloudInitNotFound: 595 fallthrough 596 default: 597 res.Action = "disable" 598 return res, DisableCloudInit(dirs.GlobalRootDir) 599 } 600 601 // from here on out, we are taking the "restrict" action 602 res.Action = "restrict" 603 604 // first get the cloud-init data-source that was used from / 605 resultsFile := filepath.Join(dirs.GlobalRootDir, "/run/cloud-init/status.json") 606 607 f, err := os.Open(resultsFile) 608 if err != nil { 609 return res, err 610 } 611 defer f.Close() 612 613 var stat cloudInitStatus 614 err = json.NewDecoder(f).Decode(&stat) 615 if err != nil { 616 return res, err 617 } 618 619 // if the datasource was empty then cloud-init did something wrong or 620 // perhaps it incorrectly reported that it ran but something else deleted 621 // the file 622 datasourceRaw := stat.V1.DataSource 623 if datasourceRaw == "" { 624 return res, fmt.Errorf("cloud-init error: missing datasource from status.json") 625 } 626 627 // for some datasources there is additional data in this item, i.e. for 628 // NoCloud we will also see: 629 // "DataSourceNoCloud [seed=/dev/sr0][dsmode=net]" 630 // so hence we use a regexp to parse out just the name of the datasource 631 datasourceMatches := datasourceRe.FindStringSubmatch(datasourceRaw) 632 if len(datasourceMatches) != 2 { 633 return res, fmt.Errorf("cloud-init error: unexpected datasource format %q", datasourceRaw) 634 } 635 res.DataSource = datasourceMatches[1] 636 637 cloudInitRestrictFile := filepath.Join(dirs.GlobalRootDir, cloudInitSnapdRestrictFile) 638 639 switch { 640 case opts.DisableAfterLocalDatasourcesRun && strutil.ListContains(localDatasources, res.DataSource): 641 // On UC20, DisableAfterLocalDatasourcesRun will be set, where we want 642 // to disable local sources like NoCloud and None after first-boot 643 // instead of just restricting them like we do below for UC16 and UC18. 644 645 // as such, change the action taken to disable and disable cloud-init 646 res.Action = "disable" 647 err = DisableCloudInit(dirs.GlobalRootDir) 648 case res.DataSource == "NoCloud": 649 // With the NoCloud datasource (which is one of the local datasources), 650 // we also need to restrict/disable the import of arbitrary filesystem 651 // labels to use as datasources, i.e. a USB drive inserted by an 652 // attacker with label CIDATA will defeat security measures on Ubuntu 653 // Core, so with the additional fs_label spec, we disable that import. 654 err = ioutil.WriteFile(cloudInitRestrictFile, nocloudRestrictYaml, 0644) 655 default: 656 // all other cases are either not local on UC20, or not NoCloud and as 657 // such we simply restrict cloud-init to the specific datasource used so 658 // that an attack via NoCloud is protected against 659 yaml := []byte(fmt.Sprintf(genericCloudRestrictYamlPattern, res.DataSource)) 660 err = ioutil.WriteFile(cloudInitRestrictFile, yaml, 0644) 661 } 662 663 return res, err 664 }