Merge pull request #3750 from stgraber/security

Security fixes for Incus 7.3
This commit is contained in:
Stéphane Graber 2026-07-30 19:42:36 -04:00 committed by GitHub
commit c6f01c862f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 268 additions and 50 deletions

View File

@ -326,12 +326,18 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m
} }
// Expansion function to deal with project inheritance. // Expansion function to deal with project inheritance.
var expandProjectErr error
expandProject := func(projectName string) string { expandProject := func(projectName string) string {
// Object types that aren't part of projects. // Object types that aren't part of projects.
if slices.Contains([]auth.ObjectType{auth.ObjectTypeUser, auth.ObjectTypeServer, auth.ObjectTypeCertificate, auth.ObjectTypeStoragePool, auth.ObjectTypeNetworkIntegration}, objectType) { if slices.Contains([]auth.ObjectType{auth.ObjectTypeUser, auth.ObjectTypeServer, auth.ObjectTypeCertificate, auth.ObjectTypeStoragePool, auth.ObjectTypeNetworkIntegration}, objectType) {
return projectName 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. // Load the project.
var p *api.Project var p *api.Project
err := d.db.Cluster.Transaction(r.Context(), func(ctx context.Context, tx *db.ClusterTx) error { 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 { if objectType == auth.ObjectTypeProfile {
projectName = project.ProfileProjectFromRecord(p) return project.ProfileProjectFromRecord(p)
} else if objectType == auth.ObjectTypeStorageBucket { } else if objectType == auth.ObjectTypeStorageBucket {
projectName = project.StorageBucketProjectFromRecord(p) return project.StorageBucketProjectFromRecord(p)
} else if objectType == auth.ObjectTypeStorageVolume { } else if objectType == auth.ObjectTypeStorageVolume {
dbVolType, err := storagePools.VolumeTypeNameToDBType(muxVars[1]) dbVolType, err := storagePools.VolumeTypeNameToDBType(muxVars[1])
if err != nil { if err != nil {
return projectName return projectName
} }
projectName = project.StorageVolumeProjectFromRecord(p, dbVolType) return project.StorageVolumeProjectFromRecord(p, dbVolType)
} else if objectType == auth.ObjectTypeNetworkZone { } else if objectType == auth.ObjectTypeNetworkZone {
projectName = project.NetworkZoneProjectFromRecord(p) return project.NetworkZoneProjectFromRecord(p)
} else if slices.Contains([]auth.ObjectType{auth.ObjectTypeImage, auth.ObjectTypeImageAlias}, objectType) { } else if slices.Contains([]auth.ObjectType{auth.ObjectTypeImage, auth.ObjectTypeImageAlias}, objectType) {
projectName = project.ImageProjectFromRecord(p) return project.ImageProjectFromRecord(p)
} else if slices.Contains([]auth.ObjectType{auth.ObjectTypeNetwork, auth.ObjectTypeNetworkACL}, objectType) { } else if slices.Contains([]auth.ObjectType{auth.ObjectTypeNetwork, auth.ObjectTypeNetworkACL, auth.ObjectTypeNetworkAddressSet}, objectType) {
projectName = project.NetworkProjectFromRecord(p) 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 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)) 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() s := d.State()
// Validate whether the user has the needed permission // Validate whether the user has the needed permission

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"slices" "slices"
"time" "time"
@ -344,13 +345,12 @@ func imageDownload(ctx context.Context, r *http.Request, s *state.State, op *ope
logger.Info("Downloading image", ctxMap) logger.Info("Downloading image", ctxMap)
// For the direct protocol the fingerprint is caller-controlled, so validate // The fingerprint is used as a file name, so reject anything that isn't a
// it to avoid path traversal when used as a file name. // partial or full hex fingerprint to avoid path traversal. It's re-validated
if protocol == "direct" { // as a full SHA-256 once expanded from the remote below.
err = validate.IsSHA256(fp) match, _ := regexp.MatchString("^[0-9a-f]{1,64}$", fp)
if err != nil { if !match {
return nil, false, errors.New("Invalid image fingerprint") return nil, false, errors.New("Invalid image fingerprint")
}
} }
// Cleanup any leftover from a past attempt // Cleanup any leftover from a past attempt
@ -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 // Image is in the DB now, don't wipe on-disk files on failure
failure = false 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) // Check if the image path changed (private images)
newDestName := filepath.Join(destDir, fp) newDestName := filepath.Join(destDir, fp)
if newDestName != destName { if newDestName != destName {

View File

@ -3,6 +3,7 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"os" "os"
"path/filepath" "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") 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 // Read exec record-output files
dents, err := os.ReadDir(inst.ExecOutputPath()) dents, err := fs.ReadDir(root.FS(), ".")
if err != nil { if err != nil {
return response.SmartError(err) return response.SmartError(err)
} }
@ -554,13 +563,40 @@ func instanceExecOutputGet(d *Daemon, r *http.Request) response.Response {
} }
reverter.Add(func() { _ = pool.UnmountInstance(inst, nil) }) 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() cleanup := reverter.Clone()
reverter.Success() reverter.Success()
ent := response.FileResponseEntry{ ent := response.FileResponseEntry{
Path: filepath.Join(inst.ExecOutputPath(), file), File: f,
Filename: file, FileSize: fi.Size(),
Cleanup: cleanup.Fail, FileModified: fi.ModTime(),
Filename: file,
Cleanup: cleanup.Fail,
} }
s.Events.SendLifecycle(projectName, lifecycle.InstanceLogRetrieved.Event(file, inst, request.CreateRequestor(r), nil)) s.Events.SendLifecycle(projectName, lifecycle.InstanceLogRetrieved.Event(file, inst, request.CreateRequestor(r), nil))
@ -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") 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 { if err != nil {
return response.SmartError(err) return response.SmartError(err)
} }

View File

@ -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") defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, c, nil) }, "Failed to unmount instance")
// If missing, just return empty result // Confine metadata access to the instance directory.
metadataPath := filepath.Join(c.Path(), "metadata.yaml") root, err := os.OpenRoot(c.Path())
if !util.PathExists(metadataPath) { if err != nil {
return response.SyncResponse(true, api.ImageMetadata{}) return response.SmartError(err)
} }
// Read the metadata defer logger.WarnOnError(root.Close, "Failed to close instance root")
metadataFile, err := os.Open(metadataPath)
// Read the metadata (if missing, just return empty result).
metadataFile, err := root.Open("metadata.yaml")
if err != nil { if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return response.SyncResponse(true, api.ImageMetadata{})
}
return response.InternalError(err) 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") defer logger.WarnOnError(func() error { return storagePools.InstanceUnmount(pool, inst, nil) }, "Failed to unmount instance")
// Read the existing data. // Confine metadata access to the instance directory.
metadataPath := filepath.Join(inst.Path(), "metadata.yaml") root, err := os.OpenRoot(inst.Path())
metadata := api.ImageMetadata{} if err != nil {
if util.PathExists(metadataPath) { return response.SmartError(err)
metadataFile, err := os.Open(metadataPath) }
if err != nil {
return response.InternalError(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") defer logger.WarnOnError(metadataFile.Close, "Failed to close metadata file")
data, err := io.ReadAll(metadataFile) data, err := io.ReadAll(metadataFile)
@ -376,9 +389,15 @@ func doInstanceMetadataUpdate(s *state.State, inst instance.Instance, metadata a
return response.BadRequest(err) return response.BadRequest(err)
} }
// Update the metadata. // Update the metadata (confined to the instance directory).
metadataPath := filepath.Join(inst.Path(), "metadata.yaml") root, err := os.OpenRoot(inst.Path())
err = os.WriteFile(metadataPath, data, 0o644) 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 { if err != nil {
return response.InternalError(err) return response.InternalError(err)
} }

View File

@ -647,6 +647,15 @@ func migrateInstance(ctx context.Context, s *state.State, inst instance.Instance
targetInstInfo.Profiles = req.Profiles 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. // Handle storage pool override.
if req.Pool != "" { if req.Pool != "" {
err := s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { err := s.DB.Cluster.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error {

View File

@ -709,6 +709,15 @@ func createFromCopy(ctx context.Context, s *state.State, r *http.Request, projec
req.Devices[key] = value 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 { if req.Stateful {
sourceName, _, _ := api.GetParentAndSnapshotName(source.Name()) sourceName, _, _ := api.GetParentAndSnapshotName(source.Name())
if sourceName != req.Name { if sourceName != req.Name {
@ -888,6 +897,19 @@ func createFromBackup(s *state.State, r *http.Request, projectName string, data
bInfo.Name = instanceName 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. // Override config.
configMap := map[string]string{} configMap := map[string]string{}
if config != "" { if config != "" {

View File

@ -2770,6 +2770,12 @@ func createStoragePoolVolumeFromISO(s *state.State, r *http.Request, requestProj
return response.BadRequest(errors.New("Missing volume name")) 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. // Create isos directory if needed.
if !util.PathExists(internalUtil.VarPath("isos")) { if !util.PathExists(internalUtil.VarPath("isos")) {
err := os.MkdirAll(internalUtil.VarPath("isos"), 0o644) err := os.MkdirAll(internalUtil.VarPath("isos"), 0o644)
@ -2937,6 +2943,19 @@ func createStoragePoolVolumeFromBackup(s *state.State, r *http.Request, requestP
bInfo.Name = volName 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{ logger.Debug("Backup file info loaded", logger.Ctx{
"type": bInfo.Type, "type": bInfo.Type,
"name": bInfo.Name, "name": bInfo.Name,

View File

@ -18,6 +18,24 @@ func IsUserConfig(key string) bool {
return strings.HasPrefix(key, "user.") 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. // ConfigVolatilePrefix indicates the prefix used for volatile config keys.
const ConfigVolatilePrefix = "volatile." const ConfigVolatilePrefix = "volatile."
@ -755,7 +773,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
// liveupdate: no // liveupdate: no
// condition: container // condition: container
// shortdesc: What driver capabilities the instance needs // 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) // gendoc:generate(entity=instance, group=nvidia, key=nvidia.require.cuda)
// The specified version expression is used to set `libnvidia-container 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 // liveupdate: no
// condition: container // condition: container
// shortdesc: Required CUDA version // shortdesc: Required CUDA version
"nvidia.require.cuda": validate.IsAny, "nvidia.require.cuda": isNvidiaConfigValue,
// gendoc:generate(entity=instance, group=nvidia, key=nvidia.require.driver) // gendoc:generate(entity=instance, group=nvidia, key=nvidia.require.driver)
// The specified version expression is used to set `libnvidia-container 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 // liveupdate: no
// condition: container // condition: container
// shortdesc: Required driver version // shortdesc: Required driver version
"nvidia.require.driver": validate.IsAny, "nvidia.require.driver": isNvidiaConfigValue,
// gendoc:generate(entity=instance, group=oci, key=oci.entrypoint) // gendoc:generate(entity=instance, group=oci, key=oci.entrypoint)
// Override the entry point of an OCI container. // Override the entry point of an OCI container.
@ -827,7 +845,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
// liveupdate: no // liveupdate: no
// condition: OCI container // condition: OCI container
// shortdesc: DNS domain // shortdesc: DNS domain
"oci.dns.domain": validate.IsAny, "oci.dns.domain": validate.Optional(isResolvConfValue),
// gendoc:generate(entity=instance, group=oci, key=oci.dns.search) // gendoc:generate(entity=instance, group=oci, key=oci.dns.search)
// Comma-separated list of search domains for the initial `resolv.conf`. // 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 // liveupdate: no
// condition: OCI container // condition: OCI container
// shortdesc: DNS search domains // 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. // Caller is responsible for full validation of any raw.* value.

View File

@ -1143,7 +1143,7 @@ func (d *lxc) initLXC(config bool) (*liblxc.Container, error) {
} }
nvidiaRequireCuda := d.expandedConfig["nvidia.require.cuda"] nvidiaRequireCuda := d.expandedConfig["nvidia.require.cuda"]
if nvidiaRequireCuda == "" { if nvidiaRequireCuda != "" {
err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("\"NVIDIA_REQUIRE_CUDA=%s\"", nvidiaRequireCuda)) err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("\"NVIDIA_REQUIRE_CUDA=%s\"", nvidiaRequireCuda))
if err != nil { if err != nil {
return nil, err return nil, err
@ -1151,7 +1151,7 @@ func (d *lxc) initLXC(config bool) (*liblxc.Container, error) {
} }
nvidiaRequireDriver := d.expandedConfig["nvidia.require.driver"] nvidiaRequireDriver := d.expandedConfig["nvidia.require.driver"]
if nvidiaRequireDriver == "" { if nvidiaRequireDriver != "" {
err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("\"NVIDIA_REQUIRE_DRIVER=%s\"", nvidiaRequireDriver)) err = lxcSetConfigItem(cc, "lxc.environment", fmt.Sprintf("\"NVIDIA_REQUIRE_DRIVER=%s\"", nvidiaRequireDriver))
if err != nil { if err != nil {
return nil, err return nil, err
@ -2617,17 +2617,25 @@ func (d *lxc) startCommon() (string, []func() error, error) {
} }
// Configure network handling. // 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 { if err != nil {
return "", nil, err 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) err = os.MkdirAll(filepath.Join(d.RootfsPath(), "etc"), 0o755)
if err != nil && !os.IsExist(err) { if err != nil && !os.IsExist(err) {
return "", nil, 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 127.0.1.1 %s
::1 localhost ip6-localhost ip6-loopback ::1 localhost ip6-localhost ip6-loopback
@ -2645,7 +2653,7 @@ ff02::2 ip6-allrouters
return "", nil, err 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 { if err != nil {
return "", nil, err return "", nil, err
} }
@ -2669,7 +2677,7 @@ ff02::2 ip6-allrouters
fmt.Fprintf(&resolvConf, "domain %s\n", d.expandedConfig["oci.dns.domain"]) 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 { if err != nil {
return "", nil, err return "", nil, err
} }
@ -2699,7 +2707,7 @@ ff02::2 ip6-allrouters
return "", nil, err 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 { if err != nil {
return "", nil, err return "", nil, err
} }

View File

@ -3777,6 +3777,14 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e
instanceMeta["ephemeral"] = "false" 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. // Go through the templates.
for tplPath, tpl := range metadata.Templates { for tplPath, tpl := range metadata.Templates {
err = func(tplPath string, tpl *api.ImageMetadataTemplate) error { err = func(tplPath string, tpl *api.ImageMetadataTemplate) error {
@ -3789,8 +3797,31 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e
return nil 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. // 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 { if err != nil {
return err return err
} }

View File

@ -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") 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. // Add the volume being created.
info.Volumes = append(info.Volumes, db.StorageVolumeArgs{ info.Volumes = append(info.Volumes, db.StorageVolumeArgs{
Name: req.Name, Name: req.Name,
@ -585,6 +598,7 @@ func checkRestrictions(project api.Project, instances []api.Instance, profiles [
allowContainerLowLevel := false allowContainerLowLevel := false
allowVMLowLevel := false allowVMLowLevel := false
blockVMNesting := false blockVMNesting := false
requireIsolated := false
var allowedIDMapHostUIDs, allowedIDMapHostGIDs []idmap.Entry var allowedIDMapHostUIDs, allowedIDMapHostGIDs []idmap.Entry
for i := range allRestrictions { for i := range allRestrictions {
@ -633,6 +647,8 @@ func checkRestrictions(project api.Project, instances []api.Instance, profiles [
return nil return nil
} }
requireIsolated = restrictionValue == "isolated"
containerConfigChecks["security.idmap.isolated"] = func(instanceValue string) error { containerConfigChecks["security.idmap.isolated"] = func(instanceValue string) error {
if restrictionValue == "isolated" && util.IsFalseOrEmpty(instanceValue) { if restrictionValue == "isolated" && util.IsFalseOrEmpty(instanceValue) {
return errors.New("Non-isolated containers are forbidden") 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) 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 { for key, value := range config {
if ((isContainerOrProfile && !allowContainerLowLevel) || (isVMOrProfile && !allowVMLowLevel)) && key == "raw.idmap" { if ((isContainerOrProfile && !allowContainerLowLevel) || (isVMOrProfile && !allowVMLowLevel)) && key == "raw.idmap" {
// If the low-level raw.idmap is used check whether the raw.idmap host IDs // If the low-level raw.idmap is used check whether the raw.idmap host IDs

View File

@ -7216,9 +7216,17 @@ func (b *backend) UpdateInstanceBackupFile(inst instance.Instance, snapshots boo
// Update pool information in the backup.yaml file. // Update pool information in the backup.yaml file.
err = vol.MountTask(func(mountPath string, op *operations.Operation) error { 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 // Write the YAML
path := filepath.Join(inst.Path(), "backup.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 { if err != nil {
return fmt.Errorf("Failed to create file %q: %w", path, err) return fmt.Errorf("Failed to create file %q: %w", path, err)
} }