Artem Chernyshev 78544a8557
feat: restrict directories for included files in the cluster templates
By default only allow to include files from the same directory where the
template file lives.
This is to prevent malicious cluster templates that include something
like `/etc/passwd`.
Fixes: https://github.com/siderolabs/omni/issues/2590

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
2026-04-16 19:28:33 +03:00

181 lines
5.6 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package models
import (
"fmt"
"strings"
"time"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/hashicorp/go-multierror"
"github.com/siderolabs/gen/pair"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/siderolabs/omni/client/api/omni/specs"
"github.com/siderolabs/omni/client/pkg/constants"
"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
)
// KindCluster is Cluster model kind.
const KindCluster = "Cluster"
// Cluster is a top-level template object.
type Cluster struct { //nolint:govet
Meta `yaml:",inline"`
SystemExtensions `yaml:",inline"`
// Name is the name of the cluster.
Name string `yaml:"name"`
// Descriptors are the user descriptors to apply to the cluster.
Descriptors Descriptors `yaml:",inline"`
// Kubernetes settings.
Kubernetes KubernetesCluster `yaml:"kubernetes"`
// Talos settings.
Talos TalosCluster `yaml:"talos"`
// Features settings.
Features Features `yaml:"features,omitempty"`
// Cluster-wide patches.
Patches PatchList `yaml:"patches,omitempty"`
// KernelArgs are the additional kernel arguments.
KernelArgs KernelArgs `yaml:",inline"`
}
// Features defines cluster-wide features.
type Features struct {
// DiskEncryption enables KMS encryption.
DiskEncryption bool `yaml:"diskEncryption,omitempty"`
// EnableWorkloadProxy enables workload proxy.
EnableWorkloadProxy bool `yaml:"enableWorkloadProxy,omitempty"`
// UseEmbeddedDiscoveryService enables the use of embedded discovery service.
UseEmbeddedDiscoveryService bool `yaml:"useEmbeddedDiscoveryService,omitempty"`
// BackupConfiguration contains backup configuration settings.
BackupConfiguration BackupConfiguration `yaml:"backupConfiguration,omitempty"`
}
// BackupConfiguration contains backup configuration settings.
type BackupConfiguration struct {
// Interval configures intervals between backups. If set to 0, etcd backups for this cluster are disabled.
Interval time.Duration `yaml:"interval,omitempty"`
}
// KubernetesCluster is a Kubernetes cluster settings.
type KubernetesCluster struct {
// Version is the Kubernetes version.
Version string `yaml:"version"`
// Manifests allow defining Kubernetes manifests to be applied to the deployed Omni cluster.
Manifests KubernetesManifestsList `yaml:"manifests,omitempty"`
}
// TalosCluster is a Talos cluster settings.
type TalosCluster struct {
// Version is the Talos version.
Version string `yaml:"version"`
}
// Validate the model.
func (cluster *Cluster) Validate(opts ValidateOptions) error {
var multiErr error
validator := omni.ClusterValidator{
ID: cluster.Name,
KubernetesVersion: cluster.Kubernetes.Version,
TalosVersion: cluster.Talos.Version,
EncryptionEnabled: cluster.Features.DiskEncryption,
RequireVPrefixOnTalosVersionCheck: true,
}
if err := validator.Validate(); err != nil {
multiErr = multierror.Append(multiErr, err)
}
if err := cluster.Descriptors.Validate(); err != nil {
multiErr = multierror.Append(multiErr, err)
}
if err := cluster.Patches.Validate(opts); err != nil {
multiErr = multierror.Append(multiErr, err)
}
if err := cluster.Kubernetes.Manifests.Validate(opts); err != nil {
multiErr = multierror.Append(multiErr, err)
}
if multiErr != nil {
return fmt.Errorf("error validating cluster %q: %w", cluster.Name, multiErr)
}
return nil
}
// Translate into Omni resources.
func (cluster *Cluster) Translate(ctx TranslateContext) ([]resource.Resource, error) {
clusterResource := omni.NewCluster(cluster.Name)
clusterResource.Metadata().Annotations().Set(omni.ResourceManagedByClusterTemplates, "")
cluster.Descriptors.Apply(clusterResource)
clusterResource.TypedSpec().Value.Features = &specs.ClusterSpec_Features{
EnableWorkloadProxy: cluster.Features.EnableWorkloadProxy,
UseEmbeddedDiscoveryService: cluster.Features.UseEmbeddedDiscoveryService,
}
clusterResource.TypedSpec().Value.KubernetesVersion = strings.TrimLeft(cluster.Kubernetes.Version, "v")
clusterResource.TypedSpec().Value.TalosVersion = strings.TrimLeft(cluster.Talos.Version, "v")
patches, err := cluster.Patches.Translate(ctx, fmt.Sprintf("cluster-%s", cluster.Name), constants.PatchBaseWeightCluster, pair.MakePair(omni.LabelCluster, cluster.Name))
if err != nil {
return nil, err
}
manifests, err := cluster.Kubernetes.Manifests.Translate(ctx, fmt.Sprintf("cluster-%s", cluster.Name), constants.PatchBaseWeightCluster,
pair.MakePair(omni.LabelCluster, cluster.Name),
)
if err != nil {
return nil, err
}
if clusterResource.TypedSpec().Value.Features == nil {
clusterResource.TypedSpec().Value.Features = &specs.ClusterSpec_Features{}
}
clusterResource.TypedSpec().Value.Features.DiskEncryption = cluster.Features.DiskEncryption
if interval := cluster.Features.BackupConfiguration.Interval; interval > 0 {
clusterResource.TypedSpec().Value.BackupConfiguration = &specs.EtcdBackupConf{Interval: durationpb.New(interval), Enabled: true}
}
resourceList := append([]resource.Resource{clusterResource}, patches...)
resourceList = append(resourceList, manifests...)
schematicConfigurations := cluster.translate(
ctx,
cluster.Name,
)
return append(resourceList, schematicConfigurations...), nil
}
func init() {
register[Cluster](KindCluster)
}
func joinErrors(err error, errs ...error) error {
for _, e := range errs {
if e != nil {
err = multierror.Append(err, e)
}
}
return err
}