Skip to content

Commit

Permalink
Merge pull request canonical#10573 from anonymouse64/feature/cloud-in…
Browse files Browse the repository at this point in the history
…it-grade-signed-filtered

sysconfig/cloud-init: filter MAAS c-i config from ubuntu-seed on grade signed
  • Loading branch information
mvo5 authored Sep 22, 2021
2 parents b99d7b6 + 0f4c772 commit 673cd60
Show file tree
Hide file tree
Showing 10 changed files with 826 additions and 100 deletions.
233 changes: 211 additions & 22 deletions sysconfig/cloudinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ type supportedFilteredReporting struct {
TokenSecret string `yaml:"token_secret,omitempty"`
}

// supportedFilteredDatasources is the set of datasources we support filtering
// cloud-init config for. It is expected that this list grows as we support for
// more clouds.
var supportedFilteredDatasources = []string{
"MAAS",
}

// filterCloudCfg filters a cloud-init configuration struct parsed from a single
// cloud-init configuration file. The config provided here may be a subset of
// the full cloud-init configuration from the file in that there may be
Expand Down Expand Up @@ -144,6 +151,23 @@ func filterCloudCfg(cfg *supportedFilteredCloudConfig, allowedDatasources []stri
// returned if the input file was entirely filtered out and there is nothing
// left.
func filterCloudCfgFile(in string, allowedDatasources []string) (string, error) {
// we don't allow any files to be installed/filtered from ubuntu-seed if
// there are no datasources at all
if len(allowedDatasources) == 0 {
return "", nil
}

// otherwise if there are datasources that are allowed, then we perform
// filtering on the file
// note that this logic means that "generic" cloud-init config which is not
// specific to a datasource will not get installed unless either:
// * there is another file specifying a datasource that intersects with the
// set of datasources mentioned in the gadget and intersects with what we
// support
// * there are no datasources mentioned in the gadget and there are other
// cloud-init files on ubuntu-seed which specify a datasource and
// intersect with what we support

dstFileName := filepath.Base(in)
filteredFile, err := ioutil.TempFile("", dstFileName)
if err != nil {
Expand Down Expand Up @@ -199,7 +223,9 @@ type cloudDatasourcesInUseResult struct {
// ExplicitlyNoneAllowed is true when datasource_list was set to
// specifically the empty list, thus disallowing use of any datasource
ExplicitlyNoneAllowed bool
// Mentioned is the full set of datasources mentioned in the yaml config.
// Mentioned is the full set of datasources mentioned in the yaml config,
// both sources from ExplicitlyAllowed and from implicitly mentioned in the
// config.
Mentioned []string
}

Expand Down Expand Up @@ -344,31 +370,60 @@ type cloudInitConfigInstallOptions struct {
// installCloudInitCfgDir installs glob cfg files from the source directory to
// the cloud config dir, optionally filtering the files for safe and supported
// keys in the configuration before installing them.
func installCloudInitCfgDir(src, targetdir string, opts *cloudInitConfigInstallOptions) error {
func installCloudInitCfgDir(src, targetdir string, opts *cloudInitConfigInstallOptions) (installedFiles []string, err error) {
if opts == nil {
opts = &cloudInitConfigInstallOptions{}
}

// TODO:UC20: enforce patterns on the glob files and their suffix ranges
ccl, err := filepath.Glob(filepath.Join(src, "*.cfg"))
if err != nil {
return err
return nil, err
}
if len(ccl) == 0 {
return nil
return nil, nil
}

ubuntuDataCloudCfgDir := filepath.Join(ubuntuDataCloudDir(targetdir), "cloud.cfg.d/")
if err := os.MkdirAll(ubuntuDataCloudCfgDir, 0755); err != nil {
return fmt.Errorf("cannot make cloud config dir: %v", err)
return nil, fmt.Errorf("cannot make cloud config dir: %v", err)
}

for _, cc := range ccl {
if err := osutil.CopyFile(cc, filepath.Join(ubuntuDataCloudCfgDir, opts.Prefix+filepath.Base(cc)), 0); err != nil {
return err
src := cc
baseName := filepath.Base(cc)
dst := filepath.Join(ubuntuDataCloudCfgDir, opts.Prefix+baseName)

if opts.Filter {
filteredFile, err := filterCloudCfgFile(cc, opts.AllowedDatasources)
if err != nil {
return nil, fmt.Errorf("error while filtering cloud-config file %s: %v", baseName, err)
}
src = filteredFile
}

// src may be the empty string if we were copying a file that got
// entirely emptied, in which case we shouldn't copy anything since
// there's nothing to install from this config file
if src == "" {
logger.Noticef("cloud-init config file %s was filtered out", baseName)
continue
}

if err := osutil.CopyFile(src, dst, 0); err != nil {
return nil, err
}

// make sure that the new file is world readable, since cloud-init does
// not run as root (somehow?)
if err := os.Chmod(dst, 0644); err != nil {
return nil, err
}

installedFiles = append(installedFiles, dst)
}
return nil

return installedFiles, nil
}

// installGadgetCloudInitCfg installs a single cloud-init config file from the
Expand Down Expand Up @@ -414,24 +469,39 @@ func configureCloudInit(model *asserts.Model, opts *Options) (err error) {

grade := model.Grade()

gadgetDatasourcesRes := &cloudDatasourcesInUseResult{}

// we always allow gadget cloud config, so install that first
if HasGadgetCloudConf(opts.GadgetDir) {
// then copy / install the gadget config first
gadgetCloudConf := filepath.Join(opts.GadgetDir, "cloud.conf")

// TODO: save the gadget datasource and use it below in deciding what to
// allow through for grade: signed
if _, err := installGadgetCloudInitCfg(gadgetCloudConf, WritableDefaultsDir(opts.TargetRootDir)); err != nil {
datasourcesRes, err := installGadgetCloudInitCfg(gadgetCloudConf, WritableDefaultsDir(opts.TargetRootDir))
if err != nil {
return err
}

gadgetDatasourcesRes = datasourcesRes

// we don't return here to enable also copying any cloud-init config
// from ubuntu-seed in order for both to be used simultaneously for
// example on test devices where the gadget has a gadget.yaml, but for
// testing purposes you also want to provision another user with
// ubuntu-seed cloud-init config
}

// after installing gadget config, check if we have to consider ubuntu-seed
// at all, if a source dir wasn't provided to us we can just exit early
// here, note that it's valid to allow cloud-init, but not set
// CloudInitSrcDir and not have a gadget cloud.conf, in this case cloud-init
// may pick up dynamic metadata and userdata from NoCloud sources such as a
// USB or CD-ROM drive with label CIDATA, etc. during first-boot
if opts.CloudInitSrcDir == "" {
return nil
}

// otherwise there is most likely something on ubuntu-seed

installOpts := &cloudInitConfigInstallOptions{
// set the prefix such that any ubuntu-seed config that ends up getting
// installed takes precedence over the gadget config
Expand All @@ -443,26 +513,145 @@ func configureCloudInit(model *asserts.Model, opts *Options) (err error) {
// for secured we are done, we only allow gadget cloud-config on secured
return nil
case asserts.ModelSigned:
// TODO: for grade signed, we will install ubuntu-seed config but filter
// it and ensure that the ubuntu-seed config matches the config from the
// gadget if that exists
// for now though, just return
return nil
// for grade signed, we filter config coming from ubuntu-seed
installOpts.Filter = true

// in order to decide what to allow through the filter, we need to
// consider the whole set of config files on ubuntu-seed as a single
// bundle of files and determine the datasource(s) in use there, and
// compare this with the datasource(s) we support through the gadget and
// in supportedFilteredDatasources

ubuntuSeedDatasourceRes, err := cloudDatasourcesInUseForDir(opts.CloudInitSrcDir)
if err != nil {
return err
}

// handle the various permutations for the datasources mentioned in the
// gadget
switch {
case gadgetDatasourcesRes.ExplicitlyNoneAllowed:
// no datasources were allowed, so set it to the empty list to
// disallow anything being installed
installOpts.AllowedDatasources = nil

// consider the case where the gadget explicitly allows specific
// datasources before considering any of the implicit mentions

case len(gadgetDatasourcesRes.ExplicitlyAllowed) != 0:
// allow the intersection of what the gadget explicitly allows, what
// ubuntu-seed either explicitly allows (or what it mentions), and
// what we statically support

if len(ubuntuSeedDatasourceRes.ExplicitlyAllowed) != 0 {
// use ubuntu-seed explicitly allowed in the intersection computation
installOpts.AllowedDatasources = strutil.Intersection(
supportedFilteredDatasources,
ubuntuSeedDatasourceRes.ExplicitlyAllowed,
gadgetDatasourcesRes.ExplicitlyAllowed,
)
} else if len(ubuntuSeedDatasourceRes.Mentioned) != 0 && !ubuntuSeedDatasourceRes.ExplicitlyNoneAllowed {
// use ubuntu-seed mentioned in the intersection computation
installOpts.AllowedDatasources = strutil.Intersection(
supportedFilteredDatasources,
ubuntuSeedDatasourceRes.Mentioned,
gadgetDatasourcesRes.ExplicitlyAllowed,
)
} else {
// then the ubuntu-seed datasources didn't either mention any
// datasources, or it explicitly disallowed any datasources (
// which would be weird to have config on ubuntu-seed which says
// "please ignore this other config on ubuntu-seed")
// but in any case we know a priori that the intersection will
// be empty
installOpts.AllowedDatasources = nil
}

case len(gadgetDatasourcesRes.Mentioned) != 0:
// allow the intersection of what the gadget mentions, what
// ubuntu-seed either explicitly allows (or what it mentions), and
// what we statically support

if len(ubuntuSeedDatasourceRes.ExplicitlyAllowed) != 0 {
// use ubuntu-seed explicitly allowed in the intersection computation
installOpts.AllowedDatasources = strutil.Intersection(
supportedFilteredDatasources,
ubuntuSeedDatasourceRes.ExplicitlyAllowed,
gadgetDatasourcesRes.Mentioned,
)
} else if len(ubuntuSeedDatasourceRes.Mentioned) != 0 && !ubuntuSeedDatasourceRes.ExplicitlyNoneAllowed {
// use ubuntu-seed mentioned in the intersection computation
installOpts.AllowedDatasources = strutil.Intersection(
supportedFilteredDatasources,
ubuntuSeedDatasourceRes.Mentioned,
gadgetDatasourcesRes.Mentioned,
)
} else {
// then the ubuntu-seed datasources didn't either mention any
// datasources, or it explicitly disallowed any datasources (
// which would be weird to have config on ubuntu-seed which says
// "please ignore this other config on ubuntu-seed")
// but in any case we know a priori that the intersection will
// be empty
installOpts.AllowedDatasources = nil
}

default:
// gadget had no opinion on the datasources used, so we allow the
// intersection of what ubuntu-seed explicitly allowed (or
// mentioned) with what we statically allow
if len(ubuntuSeedDatasourceRes.ExplicitlyAllowed) != 0 {
// use ubuntu-seed explicitly allowed in the intersection computation
installOpts.AllowedDatasources = strutil.Intersection(
supportedFilteredDatasources,
ubuntuSeedDatasourceRes.ExplicitlyAllowed,
)
} else if len(ubuntuSeedDatasourceRes.Mentioned) != 0 && !ubuntuSeedDatasourceRes.ExplicitlyNoneAllowed {
// use ubuntu-seed mentioned in the intersection computation
installOpts.AllowedDatasources = strutil.Intersection(
supportedFilteredDatasources,
ubuntuSeedDatasourceRes.Mentioned,
)
} else {
// then the ubuntu-seed datasources didn't either mention any
// datasources, or it explicitly disallowed any datasources (
// which would be weird to have config on ubuntu-seed which says
// "please ignore this other config on ubuntu-seed")
// but in any case we know a priori that the intersection will
// be empty
installOpts.AllowedDatasources = nil
}
}

case asserts.ModelDangerous:
// for grade dangerous we just install all the config from ubuntu-seed
installOpts.Filter = false
default:
return fmt.Errorf("internal error: unknown model assertion grade %s", grade)
}

if opts.CloudInitSrcDir != "" {
return installCloudInitCfgDir(opts.CloudInitSrcDir, WritableDefaultsDir(opts.TargetRootDir), installOpts)
// check if we will actually be able to install anything
if installOpts.Filter && len(installOpts.AllowedDatasources) == 0 {
return nil
}

// try installing the files, this is the case either where we are filtering
// and there are some files that will be filtered, or where we are not
// filtering and thus don't know anything about what files we might install,
// but we will install them all because we are in grade dangerous
installedFiles, err := installCloudInitCfgDir(opts.CloudInitSrcDir, WritableDefaultsDir(opts.TargetRootDir), installOpts)
if err != nil {
return err
}

// it's valid to allow cloud-init, but not set CloudInitSrcDir and not have
// a gadget cloud.conf, in this case cloud-init may pick up dynamic metadata
// and userdata from NoCloud sources such as a CD-ROM drive with label
// CIDATA, etc. during first-boot
if installOpts.Filter && len(installedFiles) != 0 {
// we are filtering files and we installed some, so we also need to
// install a datasource restriction file at the end just as a paranoia
// measure
yaml := []byte(fmt.Sprintf(genericCloudRestrictYamlPattern, strings.Join(installOpts.AllowedDatasources, ",")))
restrictFile := filepath.Join(ubuntuDataCloudDir(WritableDefaultsDir(opts.TargetRootDir)), "cloud.cfg.d/99_snapd_datasource.cfg")
return ioutil.WriteFile(restrictFile, yaml, 0644)
}

return nil
}
Expand Down
Loading

0 comments on commit 673cd60

Please sign in to comment.