diff --git a/completions/bash/crio b/completions/bash/crio index abaed9a0a35..663c9f26237 100755 --- a/completions/bash/crio +++ b/completions/bash/crio @@ -12,6 +12,7 @@ version wipe help h +--absent-mount-sources-to-reject --additional-devices --apparmor-profile --big-files-temporary-dir diff --git a/completions/fish/crio.fish b/completions/fish/crio.fish index d72399065ef..70e6f7ee746 100644 --- a/completions/fish/crio.fish +++ b/completions/fish/crio.fish @@ -9,6 +9,7 @@ function __fish_crio_no_subcommand --description 'Test if there has been any sub return 0 end +complete -c crio -n '__fish_crio_no_subcommand' -f -l absent-mount-sources-to-reject -r -d 'A list of paths that, when absent from the host, will cause a container creation to fail (as opposed to the current behavior of creating a directory).' complete -c crio -n '__fish_crio_no_subcommand' -f -l additional-devices -r -d 'Devices to add to the containers ' complete -c crio -n '__fish_crio_no_subcommand' -f -l apparmor-profile -r -d 'Name of the apparmor profile to be used as the runtime\'s default. This only takes effect if the user does not specify a profile via the Kubernetes Pod\'s metadata annotation.' complete -c crio -n '__fish_crio_no_subcommand' -f -l big-files-temporary-dir -r -d 'Path to the temporary directory to use for storing big files, used to store image blobs and data streams related to containers image management.' diff --git a/completions/zsh/_crio b/completions/zsh/_crio index c0200e2bc97..9ad7e402f66 100644 --- a/completions/zsh/_crio +++ b/completions/zsh/_crio @@ -7,7 +7,7 @@ it later with **--config**. Global options will modify the output.' 'version:dis _describe 'commands' cmds local -a opts - opts=('--additional-devices' '--apparmor-profile' '--big-files-temporary-dir' '--bind-mount-prefix' '--cgroup-manager' '--clean-shutdown-file' '--cni-config-dir' '--cni-default-network' '--cni-plugin-dir' '--config' '--config-dir' '--conmon' '--conmon-cgroup' '--conmon-env' '--container-attach-socket-dir' '--container-exits-dir' '--ctr-stop-timeout' '--decryption-keys-path' '--default-capabilities' '--default-env' '--default-mounts-file' '--default-runtime' '--default-sysctls' '--default-transport' '--default-ulimits' '--drop-infra-ctr' '--enable-metrics' '--enable-profile-unix-socket' '--gid-mappings' '--global-auth-file' '--grpc-max-recv-msg-size' '--grpc-max-send-msg-size' '--hooks-dir' '--image-volumes' '--infra-ctr-cpuset' '--insecure-registry' '--irqbalance-config-file' '--listen' '--log' '--log-dir' '--log-filter' '--log-format' '--log-journald' '--log-level' '--log-size-max' '--metrics-port' '--metrics-socket' '--namespaces-dir' '--no-pivot' '--pause-command' '--pause-image' '--pause-image-auth-file' '--pids-limit' '--pinns-path' '--profile' '--profile-port' '--read-only' '--registries-conf' '--registries-conf-dir' '--registry' '--root' '--runroot' '--runtimes' '--seccomp-profile' '--seccomp-use-default-when-empty' '--selinux' '--separate-pull-cgroup' '--signature-policy' '--storage-driver' '--storage-opt' '--stream-address' '--stream-enable-tls' '--stream-idle-timeout' '--stream-port' '--stream-tls-ca' '--stream-tls-cert' '--stream-tls-key' '--uid-mappings' '--version-file' '--version-file-persist' '--help' '--version') + opts=('--absent-mount-sources-to-reject' '--additional-devices' '--apparmor-profile' '--big-files-temporary-dir' '--bind-mount-prefix' '--cgroup-manager' '--clean-shutdown-file' '--cni-config-dir' '--cni-default-network' '--cni-plugin-dir' '--config' '--config-dir' '--conmon' '--conmon-cgroup' '--conmon-env' '--container-attach-socket-dir' '--container-exits-dir' '--ctr-stop-timeout' '--decryption-keys-path' '--default-capabilities' '--default-env' '--default-mounts-file' '--default-runtime' '--default-sysctls' '--default-transport' '--default-ulimits' '--drop-infra-ctr' '--enable-metrics' '--enable-profile-unix-socket' '--gid-mappings' '--global-auth-file' '--grpc-max-recv-msg-size' '--grpc-max-send-msg-size' '--hooks-dir' '--image-volumes' '--infra-ctr-cpuset' '--insecure-registry' '--irqbalance-config-file' '--listen' '--log' '--log-dir' '--log-filter' '--log-format' '--log-journald' '--log-level' '--log-size-max' '--metrics-port' '--metrics-socket' '--namespaces-dir' '--no-pivot' '--pause-command' '--pause-image' '--pause-image-auth-file' '--pids-limit' '--pinns-path' '--profile' '--profile-port' '--read-only' '--registries-conf' '--registries-conf-dir' '--registry' '--root' '--runroot' '--runtimes' '--seccomp-profile' '--seccomp-use-default-when-empty' '--selinux' '--separate-pull-cgroup' '--signature-policy' '--storage-driver' '--storage-opt' '--stream-address' '--stream-enable-tls' '--stream-idle-timeout' '--stream-port' '--stream-tls-ca' '--stream-tls-cert' '--stream-tls-key' '--uid-mappings' '--version-file' '--version-file-persist' '--help' '--version') _describe 'global options' opts return diff --git a/docs/crio.8.md b/docs/crio.8.md index d826f8f101e..eaae664cea8 100644 --- a/docs/crio.8.md +++ b/docs/crio.8.md @@ -11,6 +11,7 @@ crio - OCI-based implementation of Kubernetes Container Runtime Interface crio ``` +[--absent-mount-sources-to-reject]=[value] [--additional-devices]=[value] [--apparmor-profile]=[value] [--big-files-temporary-dir]=[value] @@ -117,6 +118,8 @@ crio [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] # GLOBAL OPTIONS +**--absent-mount-sources-to-reject**="": A list of paths that, when absent from the host, will cause a container creation to fail (as opposed to the current behavior of creating a directory). (default: []) + **--additional-devices**="": Devices to add to the containers (default: []) **--apparmor-profile**="": Name of the apparmor profile to be used as the runtime's default. This only takes effect if the user does not specify a profile via the Kubernetes Pod's metadata annotation. (default: crio-default) diff --git a/docs/crio.conf.5.md b/docs/crio.conf.5.md index 02923f757d5..52053529975 100644 --- a/docs/crio.conf.5.md +++ b/docs/crio.conf.5.md @@ -249,6 +249,9 @@ the container runtime configuration. **pinns_path**="" The path to find the pinns binary, which is needed to manage namespace lifecycle +**absent_mount_sources_to_reject**=[] + A list of paths that, when absent from the host, will cause a container creation to fail (as opposed to the current behavior of creating a directory). + ### CRIO.RUNTIME.RUNTIMES TABLE The "crio.runtime.runtimes" table defines a list of OCI compatible runtimes. The runtime to use is picked based on the runtime_handler provided by the CRI. If no runtime_handler is provided, the runtime will be picked based on the level of trust of the workload. diff --git a/internal/criocli/criocli.go b/internal/criocli/criocli.go index da84beaf04b..8f8447137c7 100644 --- a/internal/criocli/criocli.go +++ b/internal/criocli/criocli.go @@ -290,6 +290,9 @@ func mergeConfig(config *libconfig.Config, ctx *cli.Context) error { if ctx.IsSet("clean-shutdown-file") { config.CleanShutdownFile = ctx.String("clean-shutdown-file") } + if ctx.IsSet("absent-mount-sources-to-reject") { + config.AbsentMountSourcesToReject = StringSliceTrySplit(ctx, "absent-mount-sources-to-reject") + } if ctx.IsSet("enable-metrics") { config.EnableMetrics = ctx.Bool("enable-metrics") } @@ -848,6 +851,12 @@ func getCrioFlags(defConf *libconfig.Config) []cli.Flag { EnvVars: []string{"CONTAINER_CLEAN_SHUTDOWN_FILE"}, TakesFile: true, }, + &cli.StringSliceFlag{ + Name: "absent-mount-sources-to-reject", + Value: cli.NewStringSlice(defConf.AbsentMountSourcesToReject...), + Usage: "A list of paths that, when absent from the host, will cause a container creation to fail (as opposed to the current behavior of creating a directory).", + EnvVars: []string{"CONTAINER_ABSENT_MOUNT_SOURCES_TO_REJECT"}, + }, } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 6b220d85e06..688f38311ab 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -328,6 +328,10 @@ type RuntimeConfig struct { // InfraCtrCPUSet is the CPUs set that will be used to run infra containers InfraCtrCPUSet string `toml:"infra_ctr_cpuset"` + // AbsentMountSourcesToReject is a list of paths that, when absent from the host, + // will cause a container creation to fail (as opposed to the current behavior of creating a directory). + AbsentMountSourcesToReject []string `toml:"absent_mount_sources_to_reject"` + // seccompConfig is the internal seccomp configuration seccompConfig *seccomp.Config diff --git a/pkg/config/template.go b/pkg/config/template.go index bdfc2591b5e..e426c05c23e 100644 --- a/pkg/config/template.go +++ b/pkg/config/template.go @@ -360,6 +360,11 @@ func initCrioTemplateConfig(c *Config) ([]*templateConfigValue, error) { group: crioRuntimeConfig, isDefaultValue: simpleEqual(dc.DefaultRuntime, c.DefaultRuntime), }, + { + templateString: templateStringCrioRuntimeAbsentMountSourcesToReject, + group: crioRuntimeConfig, + isDefaultValue: stringSliceEqual(dc.AbsentMountSourcesToReject, c.AbsentMountSourcesToReject), + }, { templateString: templateStringCrioRuntimeRuntimesRuntimeHandler, group: crioRuntimeConfig, @@ -893,6 +898,17 @@ default_runtime = "{{ .DefaultRuntime }}" ` +const templateStringCrioRuntimeAbsentMountSourcesToReject = `# A list of paths that, when absent from the host, +# will cause a container creation to fail (as opposed to the current behavior being created as a directory). +# This option is to protect from source locations whose existence as a directory could jepordize the health of the node, and whose +# creation as a file is not desired either. +# An example is /etc/hostname, which will cause failures on reboot if it's created as a directory, but often doesn't exist because +# the hostname is being managed dynamically. +absent_mount_sources_to_reject = [ +{{ range $mount := .AbsentMountSourcesToReject}}{{ printf "\t%q,\n" $mount}}{{ end }}] + +` + const templateStringCrioRuntimeRuntimesRuntimeHandler = `# The "crio.runtime.runtimes" table defines a list of OCI compatible runtimes. # The runtime to use is picked based on the runtime_handler provided by the CRI. # If no runtime_handler is provided, the runtime will be picked based on the level diff --git a/server/container_create_linux.go b/server/container_create_linux.go index 5406dee30f7..840c724cbc6 100644 --- a/server/container_create_linux.go +++ b/server/container_create_linux.go @@ -265,7 +265,7 @@ func (s *Server) createSandboxContainer(ctx context.Context, ctr ctrIface.Contai processLabel = "" } - containerVolumes, ociMounts, err := addOCIBindMounts(ctx, mountLabel, containerConfig, specgen, s.config.RuntimeConfig.BindMountPrefix) + containerVolumes, ociMounts, err := addOCIBindMounts(ctx, mountLabel, containerConfig, specgen, s.config.RuntimeConfig.BindMountPrefix, s.config.AbsentMountSourcesToReject) if err != nil { return nil, err } @@ -770,7 +770,7 @@ func clearReadOnly(m *rspec.Mount) { m.Options = append(m.Options, "rw") } -func addOCIBindMounts(ctx context.Context, mountLabel string, containerConfig *types.ContainerConfig, specgen *generate.Generator, bindMountPrefix string) ([]oci.ContainerVolume, []rspec.Mount, error) { +func addOCIBindMounts(ctx context.Context, mountLabel string, containerConfig *types.ContainerConfig, specgen *generate.Generator, bindMountPrefix string, absentMountSourcesToReject []string) ([]oci.ContainerVolume, []rspec.Mount, error) { volumes := []oci.ContainerVolume{} ociMounts := []rspec.Mount{} mounts := containerConfig.Mounts @@ -826,7 +826,15 @@ func addOCIBindMounts(ctx context.Context, mountLabel string, containerConfig *t } else { if !os.IsNotExist(err) { return nil, nil, fmt.Errorf("failed to resolve symlink %q: %v", src, err) - } else if err = os.MkdirAll(src, 0o755); err != nil { + } + for _, toReject := range absentMountSourcesToReject { + if filepath.Clean(src) == toReject { + // special-case /etc/hostname, as we don't want it to be created as a directory + // This can cause issues with node reboot. + return nil, nil, errors.Errorf("Cannot mount %s: path does not exist and will cause issues as a directory", toReject) + } + } + if err = os.MkdirAll(src, 0o755); err != nil { return nil, nil, fmt.Errorf("failed to mkdir %s: %s", src, err) } } diff --git a/server/container_create_linux_test.go b/server/container_create_linux_test.go index 86e84df9a21..27c0d2375fb 100644 --- a/server/container_create_linux_test.go +++ b/server/container_create_linux_test.go @@ -23,7 +23,7 @@ func TestAddOCIBindsForDev(t *testing.T) { }, }, } - _, binds, err := addOCIBindMounts(context.Background(), "", config, &specgen, "") + _, binds, err := addOCIBindMounts(context.Background(), "", config, &specgen, "", nil) if err != nil { t.Error(err) } @@ -57,7 +57,7 @@ func TestAddOCIBindsForSys(t *testing.T) { }, }, } - _, binds, err := addOCIBindMounts(context.Background(), "", config, &specgen, "") + _, binds, err := addOCIBindMounts(context.Background(), "", config, &specgen, "", nil) if err != nil { t.Error(err) } diff --git a/test/ctr.bats b/test/ctr.bats index 27803542263..eff9e6f6b87 100644 --- a/test/ctr.bats +++ b/test/ctr.bats @@ -887,3 +887,18 @@ function wait_until_exit() { output=$(crictl exec --sync "$ctr_id" env) [[ "$output" == *"NSS_SDB_USE_CACHE=no"* ]] } + +@test "ctr with absent mount that should be rejected" { + ABSENT_DIR="$TESTDIR/notthere" + jq --arg path "$ABSENT_DIR" \ + ' .mounts = [ { + host_path: $path, + container_path: $path + } ]' \ + "$TESTDATA"/container_redis.json > "$TESTDIR/config" + + CONTAINER_ABSENT_MOUNT_SOURCES_TO_REJECT="$ABSENT_DIR" start_crio + + pod_id=$(crictl runp "$TESTDATA"/sandbox_config.json) + ! crictl create "$pod_id" "$TESTDIR/config" "$TESTDATA"/sandbox_config.json +}