mirror of
https://github.com/lxc/incus
synced 2026-08-02 05:26:46 +00:00
Merge pull request #3750 from stgraber/security
Security fixes for Incus 7.3
This commit is contained in:
commit
c6f01c862f
@ -326,12 +326,18 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m
|
||||
}
|
||||
|
||||
// Expansion function to deal with project inheritance.
|
||||
var expandProjectErr error
|
||||
expandProject := func(projectName string) string {
|
||||
// Object types that aren't part of projects.
|
||||
if slices.Contains([]auth.ObjectType{auth.ObjectTypeUser, auth.ObjectTypeServer, auth.ObjectTypeCertificate, auth.ObjectTypeStoragePool, auth.ObjectTypeNetworkIntegration}, objectType) {
|
||||
return projectName
|
||||
}
|
||||
|
||||
// Object types that are always addressed in the requested project.
|
||||
if slices.Contains([]auth.ObjectType{auth.ObjectTypeProject, auth.ObjectTypeInstance}, objectType) {
|
||||
return projectName
|
||||
}
|
||||
|
||||
// Load the project.
|
||||
var p *api.Project
|
||||
err := d.db.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error {
|
||||
@ -352,24 +358,26 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m
|
||||
}
|
||||
|
||||
if objectType == auth.ObjectTypeProfile {
|
||||
projectName = project.ProfileProjectFromRecord(p)
|
||||
return project.ProfileProjectFromRecord(p)
|
||||
} else if objectType == auth.ObjectTypeStorageBucket {
|
||||
projectName = project.StorageBucketProjectFromRecord(p)
|
||||
return project.StorageBucketProjectFromRecord(p)
|
||||
} else if objectType == auth.ObjectTypeStorageVolume {
|
||||
dbVolType, err := storagePools.VolumeTypeNameToDBType(muxVars[1])
|
||||
if err != nil {
|
||||
return projectName
|
||||
}
|
||||
|
||||
projectName = project.StorageVolumeProjectFromRecord(p, dbVolType)
|
||||
return project.StorageVolumeProjectFromRecord(p, dbVolType)
|
||||
} else if objectType == auth.ObjectTypeNetworkZone {
|
||||
projectName = project.NetworkZoneProjectFromRecord(p)
|
||||
return project.NetworkZoneProjectFromRecord(p)
|
||||
} else if slices.Contains([]auth.ObjectType{auth.ObjectTypeImage, auth.ObjectTypeImageAlias}, objectType) {
|
||||
projectName = project.ImageProjectFromRecord(p)
|
||||
} else if slices.Contains([]auth.ObjectType{auth.ObjectTypeNetwork, auth.ObjectTypeNetworkACL}, objectType) {
|
||||
projectName = project.NetworkProjectFromRecord(p)
|
||||
return project.ImageProjectFromRecord(p)
|
||||
} else if slices.Contains([]auth.ObjectType{auth.ObjectTypeNetwork, auth.ObjectTypeNetworkACL, auth.ObjectTypeNetworkAddressSet}, objectType) {
|
||||
return project.NetworkProjectFromRecord(p)
|
||||
}
|
||||
|
||||
// Fail closed rather than defaulting to the requested project, which could bypass confinement.
|
||||
expandProjectErr = fmt.Errorf("No project expansion defined for object type %q", objectType)
|
||||
return projectName
|
||||
}
|
||||
|
||||
@ -450,6 +458,10 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m
|
||||
return response.InternalError(fmt.Errorf("Failed to create authentication object: %w", err))
|
||||
}
|
||||
|
||||
if expandProjectErr != nil {
|
||||
return response.InternalError(fmt.Errorf("Failed to expand project for authorization: %w", expandProjectErr))
|
||||
}
|
||||
|
||||
s := d.State()
|
||||
|
||||
// Validate whether the user has the needed permission
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
@ -344,14 +345,13 @@ func imageDownload(ctx context.Context, r *http.Request, s *state.State, op *ope
|
||||
|
||||
logger.Info("Downloading image", ctxMap)
|
||||
|
||||
// For the direct protocol the fingerprint is caller-controlled, so validate
|
||||
// it to avoid path traversal when used as a file name.
|
||||
if protocol == "direct" {
|
||||
err = validate.IsSHA256(fp)
|
||||
if err != nil {
|
||||
// The fingerprint is used as a file name, so reject anything that isn't a
|
||||
// partial or full hex fingerprint to avoid path traversal. It's re-validated
|
||||
// as a full SHA-256 once expanded from the remote below.
|
||||
match, _ := regexp.MatchString("^[0-9a-f]{1,64}$", fp)
|
||||
if !match {
|
||||
return nil, false, errors.New("Invalid image fingerprint")
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup any leftover from a past attempt
|
||||
destDir := internalUtil.VarPath("images")
|
||||
@ -620,6 +620,13 @@ func imageDownload(ctx context.Context, r *http.Request, s *state.State, op *ope
|
||||
// Image is in the DB now, don't wipe on-disk files on failure
|
||||
failure = false
|
||||
|
||||
// Re-validate the fingerprint as it may have been updated from the remote
|
||||
// server's response, and it is used as a file name below.
|
||||
err = validate.IsSHA256(fp)
|
||||
if err != nil {
|
||||
return nil, false, errors.New("Invalid image fingerprint")
|
||||
}
|
||||
|
||||
// Check if the image path changed (private images)
|
||||
newDestName := filepath.Join(destDir, fp)
|
||||
if newDestName != destName {
|
||||
|
||||
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -439,8 +440,16 @@ func instanceExecOutputsGet(d *Daemon, r *http.Request) response.Response {
|
||||
|
||||
defer logger.WarnOnError(func() error { return pool.UnmountInstance(inst, nil) }, "Failed to unmount instance")
|
||||
|
||||
// Confine access to the exec output directory to avoid following symlinks.
|
||||
root, err := os.OpenRoot(inst.ExecOutputPath())
|
||||
if err != nil {
|
||||
return response.SmartError(err)
|
||||
}
|
||||
|
||||
defer logger.WarnOnError(root.Close, "Failed to close exec output root")
|
||||
|
||||
// Read exec record-output files
|
||||
dents, err := os.ReadDir(inst.ExecOutputPath())
|
||||
dents, err := fs.ReadDir(root.FS(), ".")
|
||||
if err != nil {
|
||||
return response.SmartError(err)
|
||||
}
|
||||
@ -554,11 +563,38 @@ func instanceExecOutputGet(d *Daemon, r *http.Request) response.Response {
|
||||
}
|
||||
|
||||
reverter.Add(func() { _ = pool.UnmountInstance(inst, nil) })
|
||||
|
||||
// Confine access to the exec output directory to avoid following symlinks.
|
||||
root, err := os.OpenRoot(inst.ExecOutputPath())
|
||||
if err != nil {
|
||||
return response.SmartError(err)
|
||||
}
|
||||
|
||||
reverter.Add(func() { _ = root.Close() })
|
||||
|
||||
f, err := root.Open(file)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return response.NotFound(fmt.Errorf("Exec record-output file %q not found", file))
|
||||
}
|
||||
|
||||
return response.SmartError(err)
|
||||
}
|
||||
|
||||
reverter.Add(func() { _ = f.Close() })
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return response.SmartError(err)
|
||||
}
|
||||
|
||||
cleanup := reverter.Clone()
|
||||
reverter.Success()
|
||||
|
||||
ent := response.FileResponseEntry{
|
||||
Path: filepath.Join(inst.ExecOutputPath(), file),
|
||||
File: f,
|
||||
FileSize: fi.Size(),
|
||||
FileModified: fi.ModTime(),
|
||||
Filename: file,
|
||||
Cleanup: cleanup.Fail,
|
||||
}
|
||||
@ -657,7 +693,15 @@ func instanceExecOutputDelete(d *Daemon, r *http.Request) response.Response {
|
||||
|
||||
defer logger.WarnOnError(func() error { return pool.UnmountInstance(inst, nil) }, "Failed to unmount instance")
|
||||
|
||||
err = os.Remove(filepath.Join(inst.ExecOutputPath(), file))
|
||||
// Confine access to the exec output directory to avoid following symlinks.
|
||||
root, err := os.OpenRoot(inst.ExecOutputPath())
|
||||
if err != nil {
|
||||
return response.SmartError(err)
|
||||
}
|
||||
|
||||
defer logger.WarnOnError(root.Close, "Failed to close exec output root")
|
||||
|
||||
err = root.Remove(file)
|
||||
if err != nil {
|
||||
return response.SmartError(err)
|
||||
}
|
||||
|
||||
@ -119,15 +119,21 @@ func instanceMetadataGet(d *Daemon, r *http.Request) response.Response {
|
||||
|
||||
defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, c, nil) }, "Failed to unmount instance")
|
||||
|
||||
// If missing, just return empty result
|
||||
metadataPath := filepath.Join(c.Path(), "metadata.yaml")
|
||||
if !util.PathExists(metadataPath) {
|
||||
// Confine metadata access to the instance directory.
|
||||
root, err := os.OpenRoot(c.Path())
|
||||
if err != nil {
|
||||
return response.SmartError(err)
|
||||
}
|
||||
|
||||
defer logger.WarnOnError(root.Close, "Failed to close instance root")
|
||||
|
||||
// Read the metadata (if missing, just return empty result).
|
||||
metadataFile, err := root.Open("metadata.yaml")
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return response.SyncResponse(true, api.ImageMetadata{})
|
||||
}
|
||||
|
||||
// Read the metadata
|
||||
metadataFile, err := os.Open(metadataPath)
|
||||
if err != nil {
|
||||
return response.InternalError(err)
|
||||
}
|
||||
|
||||
@ -235,15 +241,22 @@ func instanceMetadataPatch(d *Daemon, r *http.Request) response.Response {
|
||||
|
||||
defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, inst, nil) }, "Failed to unmount instance")
|
||||
|
||||
// Read the existing data.
|
||||
metadataPath := filepath.Join(inst.Path(), "metadata.yaml")
|
||||
metadata := api.ImageMetadata{}
|
||||
if util.PathExists(metadataPath) {
|
||||
metadataFile, err := os.Open(metadataPath)
|
||||
// Confine metadata access to the instance directory.
|
||||
root, err := os.OpenRoot(inst.Path())
|
||||
if err != nil {
|
||||
return response.SmartError(err)
|
||||
}
|
||||
|
||||
defer logger.WarnOnError(root.Close, "Failed to close instance root")
|
||||
|
||||
// Read the existing data.
|
||||
metadata := api.ImageMetadata{}
|
||||
metadataFile, err := root.Open("metadata.yaml")
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return response.InternalError(err)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
defer logger.WarnOnError(metadataFile.Close, "Failed to close metadata file")
|
||||
|
||||
data, err := io.ReadAll(metadataFile)
|
||||
@ -376,9 +389,15 @@ func doInstanceMetadataUpdate(s *state.State, inst instance.Instance, metadata a
|
||||
return response.BadRequest(err)
|
||||
}
|
||||
|
||||
// Update the metadata.
|
||||
metadataPath := filepath.Join(inst.Path(), "metadata.yaml")
|
||||
err = os.WriteFile(metadataPath, data, 0o644)
|
||||
// Update the metadata (confined to the instance directory).
|
||||
root, err := os.OpenRoot(inst.Path())
|
||||
if err != nil {
|
||||
return response.InternalError(err)
|
||||
}
|
||||
|
||||
defer logger.WarnOnError(root.Close, "Failed to close instance root")
|
||||
|
||||
err = root.WriteFile("metadata.yaml", data, 0o644)
|
||||
if err != nil {
|
||||
return response.InternalError(err)
|
||||
}
|
||||
|
||||
@ -647,6 +647,15 @@ func migrateInstance(ctx context.Context, s *state.State, inst instance.Instance
|
||||
targetInstInfo.Profiles = req.Profiles
|
||||
}
|
||||
|
||||
// Enforce project restrictions against the overridden config, as the migration
|
||||
// request can otherwise set restricted keys (e.g. security.privileged, raw.lxc).
|
||||
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
||||
return project.AllowInstanceUpdate(tx, inst.Project().Name, inst.Name(), targetInstInfo.Writable(), inst.LocalConfig())
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle storage pool override.
|
||||
if req.Pool != "" {
|
||||
err := s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
||||
|
||||
@ -709,6 +709,15 @@ func createFromCopy(ctx context.Context, s *state.State, r *http.Request, projec
|
||||
req.Devices[key] = value
|
||||
}
|
||||
|
||||
// Re-check project restrictions against the fully merged config, as the source
|
||||
// instance's config and devices (including restricted keys) are only merged in above.
|
||||
err = s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {
|
||||
return project.AllowInstanceCreation(tx, targetProject, *req)
|
||||
})
|
||||
if err != nil {
|
||||
return response.SmartError(err)
|
||||
}
|
||||
|
||||
if req.Stateful {
|
||||
sourceName, _, _ := api.GetParentAndSnapshotName(source.Name())
|
||||
if sourceName != req.Name {
|
||||
@ -888,6 +897,19 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data
|
||||
bInfo.Name = instanceName
|
||||
}
|
||||
|
||||
// Validate the instance and snapshot names to avoid path traversal when used as path segments.
|
||||
err = instance.ValidName(bInfo.Name, false)
|
||||
if err != nil {
|
||||
return response.BadRequest(err)
|
||||
}
|
||||
|
||||
for _, snapName := range bInfo.Snapshots {
|
||||
err = instance.ValidName(bInfo.Name+internalInstance.SnapshotDelimiter+snapName, true)
|
||||
if err != nil {
|
||||
return response.BadRequest(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override config.
|
||||
configMap := map[string]string{}
|
||||
if config != "" {
|
||||
|
||||
@ -2770,6 +2770,12 @@ func createStoragePoolVolumeFromISO(s *state.State, r *http.Request, requestProj
|
||||
return response.BadRequest(errors.New("Missing volume name"))
|
||||
}
|
||||
|
||||
// Validate the volume name to avoid path traversal when used as a path segment.
|
||||
err := validate.IsAPIName(volName, false)
|
||||
if err != nil {
|
||||
return response.BadRequest(fmt.Errorf("Invalid volume name: %w", err))
|
||||
}
|
||||
|
||||
// Create isos directory if needed.
|
||||
if !util.PathExists(internalUtil.VarPath("isos")) {
|
||||
err := os.MkdirAll(internalUtil.VarPath("isos"), 0o644)
|
||||
@ -2937,6 +2943,19 @@ func createStoragePoolVolumeFromBackup(s *state.State, r *http.Request, requestP
|
||||
bInfo.Name = volName
|
||||
}
|
||||
|
||||
// Validate the volume and snapshot names to avoid path traversal when used as path segments.
|
||||
err = validate.IsAPIName(bInfo.Name, false)
|
||||
if err != nil {
|
||||
return response.BadRequest(fmt.Errorf("Invalid volume name: %w", err))
|
||||
}
|
||||
|
||||
for _, snapName := range bInfo.Snapshots {
|
||||
err = validate.IsAPIName(snapName, false)
|
||||
if err != nil {
|
||||
return response.BadRequest(fmt.Errorf("Invalid snapshot name: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Backup file info loaded", logger.Ctx{
|
||||
"type": bInfo.Type,
|
||||
"name": bInfo.Name,
|
||||
|
||||
@ -18,6 +18,24 @@ func IsUserConfig(key string) bool {
|
||||
return strings.HasPrefix(key, "user.")
|
||||
}
|
||||
|
||||
// isNvidiaConfigValue rejects line breaks that would allow injecting arbitrary directives into the generated LXC configuration.
|
||||
func isNvidiaConfigValue(value string) error {
|
||||
if strings.ContainsAny(value, "\r\n") {
|
||||
return errors.New("NVIDIA configuration values cannot contain line breaks")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isResolvConfValue rejects line breaks that would allow injecting arbitrary lines into the generated resolv.conf.
|
||||
func isResolvConfValue(value string) error {
|
||||
if strings.ContainsAny(value, "\r\n") {
|
||||
return errors.New("Value cannot contain line breaks")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigVolatilePrefix indicates the prefix used for volatile config keys.
|
||||
const ConfigVolatilePrefix = "volatile."
|
||||
|
||||
@ -755,7 +773,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
|
||||
// liveupdate: no
|
||||
// condition: container
|
||||
// shortdesc: What driver capabilities the instance needs
|
||||
"nvidia.driver.capabilities": validate.IsAny,
|
||||
"nvidia.driver.capabilities": isNvidiaConfigValue,
|
||||
|
||||
// gendoc:generate(entity=instance, group=nvidia, key=nvidia.require.cuda)
|
||||
// The specified version expression is used to set `libnvidia-container NVIDIA_REQUIRE_CUDA`.
|
||||
@ -764,7 +782,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
|
||||
// liveupdate: no
|
||||
// condition: container
|
||||
// shortdesc: Required CUDA version
|
||||
"nvidia.require.cuda": validate.IsAny,
|
||||
"nvidia.require.cuda": isNvidiaConfigValue,
|
||||
|
||||
// gendoc:generate(entity=instance, group=nvidia, key=nvidia.require.driver)
|
||||
// The specified version expression is used to set `libnvidia-container NVIDIA_REQUIRE_DRIVER`.
|
||||
@ -773,7 +791,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
|
||||
// liveupdate: no
|
||||
// condition: container
|
||||
// shortdesc: Required driver version
|
||||
"nvidia.require.driver": validate.IsAny,
|
||||
"nvidia.require.driver": isNvidiaConfigValue,
|
||||
|
||||
// gendoc:generate(entity=instance, group=oci, key=oci.entrypoint)
|
||||
// Override the entry point of an OCI container.
|
||||
@ -827,7 +845,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
|
||||
// liveupdate: no
|
||||
// condition: OCI container
|
||||
// shortdesc: DNS domain
|
||||
"oci.dns.domain": validate.IsAny,
|
||||
"oci.dns.domain": validate.Optional(isResolvConfValue),
|
||||
|
||||
// gendoc:generate(entity=instance, group=oci, key=oci.dns.search)
|
||||
// Comma-separated list of search domains for the initial `resolv.conf`.
|
||||
@ -836,7 +854,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
|
||||
// liveupdate: no
|
||||
// condition: OCI container
|
||||
// shortdesc: DNS search domains
|
||||
"oci.dns.search": validate.IsAny,
|
||||
"oci.dns.search": validate.Optional(validate.IsListOf(isResolvConfValue)),
|
||||
|
||||
// Caller is responsible for full validation of any raw.* value.
|
||||
|
||||
|
||||
@ -1143,7 +1143,7 @@ func (d *lxc) initLXC(config bool) (*liblxc.Container, error) {
|
||||
}
|
||||
|
||||
nvidiaRequireCuda := d.expandedConfig["nvidia.require.cuda"]
|
||||
if nvidiaRequireCuda == "" {
|
||||
if nvidiaRequireCuda != "" {
|
||||
err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("\"NVIDIA_REQUIRE_CUDA=%s\"", nvidiaRequireCuda))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1151,7 +1151,7 @@ func (d *lxc) initLXC(config bool) (*liblxc.Container, error) {
|
||||
}
|
||||
|
||||
nvidiaRequireDriver := d.expandedConfig["nvidia.require.driver"]
|
||||
if nvidiaRequireDriver == "" {
|
||||
if nvidiaRequireDriver != "" {
|
||||
err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("\"NVIDIA_REQUIRE_DRIVER=%s\"", nvidiaRequireDriver))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -2617,17 +2617,25 @@ func (d *lxc) startCommon() (string, []func() error, error) {
|
||||
}
|
||||
|
||||
// Configure network handling.
|
||||
err = os.MkdirAll(filepath.Join(d.Path(), "network"), 0o711)
|
||||
// Confine all writes to the instance directory to avoid following image-planted symlinks.
|
||||
instRoot, err := os.OpenRoot(d.Path())
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
defer logger.WarnOnError(instRoot.Close, "Failed to close instance root")
|
||||
|
||||
err = instRoot.Mkdir("network", 0o711)
|
||||
if err != nil && !errors.Is(err, fs.ErrExist) {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Join(d.RootfsPath(), "etc"), 0o755)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(d.Path(), "network", "hosts"), fmt.Appendf(nil, `127.0.0.1 localhost
|
||||
err = instRoot.WriteFile("network/hosts", fmt.Appendf(nil, `127.0.0.1 localhost
|
||||
127.0.1.1 %s
|
||||
|
||||
::1 localhost ip6-localhost ip6-loopback
|
||||
@ -2645,7 +2653,7 @@ ff02::2 ip6-allrouters
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(d.Path(), "network", "hostname"), fmt.Appendf(nil, "%s\n", d.name), 0o644)
|
||||
err = instRoot.WriteFile("network/hostname", fmt.Appendf(nil, "%s\n", d.name), 0o644)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@ -2669,7 +2677,7 @@ ff02::2 ip6-allrouters
|
||||
fmt.Fprintf(&resolvConf, "domain %s\n", d.expandedConfig["oci.dns.domain"])
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(d.Path(), "network", "resolv.conf"), []byte(resolvConf.String()), 0o644)
|
||||
err = instRoot.WriteFile("network/resolv.conf", []byte(resolvConf.String()), 0o644)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@ -2699,7 +2707,7 @@ ff02::2 ip6-allrouters
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(d.Path(), "network", "interfaces.json"), ifacesData, 0o644)
|
||||
err = instRoot.WriteFile("network/interfaces.json", ifacesData, 0o644)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
@ -3777,6 +3777,14 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e
|
||||
instanceMeta["ephemeral"] = "false"
|
||||
}
|
||||
|
||||
// Open the output directory as an os.Root so all template writes stay confined to it.
|
||||
outputRoot, err := os.OpenRoot(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open template output path: %w", err)
|
||||
}
|
||||
|
||||
defer logger.WarnOnError(outputRoot.Close, "Failed to close template output path")
|
||||
|
||||
// Go through the templates.
|
||||
for tplPath, tpl := range metadata.Templates {
|
||||
err = func(tplPath string, tpl *api.ImageMetadataTemplate) error {
|
||||
@ -3789,8 +3797,31 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform some early security checks on the template itself.
|
||||
if filepath.Base(tpl.Template) != tpl.Template {
|
||||
return errors.New("Template path is attempting to read outside of template directory")
|
||||
}
|
||||
|
||||
tplDirStat, err := os.Lstat(d.TemplatesPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't access template directory: %w", err)
|
||||
}
|
||||
|
||||
if !tplDirStat.IsDir() {
|
||||
return errors.New("Template directory isn't a regular directory")
|
||||
}
|
||||
|
||||
tplFileStat, err := os.Lstat(filepath.Join(d.TemplatesPath(), tpl.Template))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Couldn't access template file: %w", err)
|
||||
}
|
||||
|
||||
if tplFileStat.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
return errors.New("Template file is a symlink")
|
||||
}
|
||||
|
||||
// Create the file itself.
|
||||
w, err = os.Create(filepath.Join(path, fmt.Sprintf("%s.out", tpl.Template)))
|
||||
w, err = outputRoot.Create(fmt.Sprintf("%s.out", tpl.Template))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -318,6 +318,19 @@ func AllowVolumeCreation(tx *db.ClusterTx, projectName string, poolName string,
|
||||
return errors.New("Restricted projects aren't allowed to use pull mode migration")
|
||||
}
|
||||
|
||||
// Restricted projects can't override low-level volume options that are passed to
|
||||
// filesystem tooling running as root; they may only use the pool's configured default.
|
||||
if util.IsTrue(info.Project.Config["restricted"]) {
|
||||
_, pool, _, err := tx.GetStoragePool(context.Background(), poolName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Config["block.create_options"] != "" && req.Config["block.create_options"] != pool.Config["volume.block.create_options"] {
|
||||
return errors.New(`Storage volume option "block.create_options" cannot be set in a restricted project`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the volume being created.
|
||||
info.Volumes = append(info.Volumes, db.StorageVolumeArgs{
|
||||
Name: req.Name,
|
||||
@ -585,6 +598,7 @@ func checkRestrictions(project api.Project, instances []api.Instance, profiles [
|
||||
allowContainerLowLevel := false
|
||||
allowVMLowLevel := false
|
||||
blockVMNesting := false
|
||||
requireIsolated := false
|
||||
var allowedIDMapHostUIDs, allowedIDMapHostGIDs []idmap.Entry
|
||||
|
||||
for i := range allRestrictions {
|
||||
@ -633,6 +647,8 @@ func checkRestrictions(project api.Project, instances []api.Instance, profiles [
|
||||
return nil
|
||||
}
|
||||
|
||||
requireIsolated = restrictionValue == "isolated"
|
||||
|
||||
containerConfigChecks["security.idmap.isolated"] = func(instanceValue string) error {
|
||||
if restrictionValue == "isolated" && util.IsFalseOrEmpty(instanceValue) {
|
||||
return errors.New("Non-isolated containers are forbidden")
|
||||
@ -827,6 +843,11 @@ func checkRestrictions(project api.Project, instances []api.Instance, profiles [
|
||||
return fmt.Errorf(`Virtual machine nesting is forbidden on %s %q of project %q ("security.nesting" must be set to "false")`, entityTypeLabel, entityName, project.Name)
|
||||
}
|
||||
|
||||
// Non-isolation is the default, so when isolation is required, the container must explicitly enable it.
|
||||
if requireIsolated && instType == instancetype.Container && util.IsFalseOrEmpty(config["security.idmap.isolated"]) {
|
||||
return fmt.Errorf(`Non-isolated containers are forbidden on %s %q of project %q ("security.idmap.isolated" must be set to "true")`, entityTypeLabel, entityName, project.Name)
|
||||
}
|
||||
|
||||
for key, value := range config {
|
||||
if ((isContainerOrProfile && !allowContainerLowLevel) || (isVMOrProfile && !allowVMLowLevel)) && key == "raw.idmap" {
|
||||
// If the low-level raw.idmap is used check whether the raw.idmap host IDs
|
||||
|
||||
@ -7216,9 +7216,17 @@ func (b *backend) UpdateInstanceBackupFile(inst instance.Instance, snapshots boo
|
||||
|
||||
// Update pool information in the backup.yaml file.
|
||||
err = vol.MountTask(func(mountPath string, op *operations.Operation) error {
|
||||
// Confine the write to the instance directory to avoid following image-planted symlinks.
|
||||
root, err := os.OpenRoot(inst.Path())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = root.Close() }()
|
||||
|
||||
// Write the YAML
|
||||
path := filepath.Join(inst.Path(), "backup.yaml")
|
||||
f, err := os.Create(path)
|
||||
f, err := root.OpenFile("backup.yaml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o400)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create file %q: %w", path, err)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user