mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 23:15:14 +00:00
Merge pull request #27180 from sttts/sysctl-implementation
Automatic merge from submit-queue
Add sysctl support
Implementation of proposal https://github.com/kubernetes/kubernetes/pull/26057, feature https://github.com/kubernetes/features/issues/34
TODO:
- [x] change types.go
- [x] implement docker and rkt support
- [x] add e2e tests
- [x] decide whether we want apiserver validation
- ~~[ ] add documentation~~: api docs exist. Existing PodSecurityContext docs is very light and links back to the api docs anyway: 6684555ed9/docs/user-guide/security-context.md
- [x] change PodSecurityPolicy in types.go
- [x] write admission controller support for PodSecurityPolicy
- [x] write e2e test for PodSecurityPolicy
- [x] make sure we are compatible in the sense of https://github.com/kubernetes/kubernetes/blob/master/docs/devel/api_changes.md
- [x] test e2e with rkt: it only works with kubenet, not with no-op network plugin. The later has no sysctl support.
- ~~[ ] add RunC implementation~~ (~~if that is already in kube,~~ it isn't)
- [x] update whitelist
- [x] switch PSC fields to annotations
- [x] switch PSP fields to annotations
- [x] decide about `--experimental-whitelist-sysctl` flag to be additive or absolute
- [x] decide whether to add a sysctl node whitelist annotation
### Release notes:
```release-note
The pod annotation `security.alpha.kubernetes.io/sysctls` now allows customization of namespaced and well isolated kernel parameters (sysctls), starting with `kernel.shm_rmid_forced`, `net.ipv4.ip_local_port_range`, `net.ipv4.tcp_max_syn_backlog` and `net.ipv4.tcp_syncookies` for Kubernetes 1.4.
The pod annotation `security.alpha.kubernetes.io/unsafeSysctls` allows customization of namespaced sysctls where isolation is unclear. Unsafe sysctls must be enabled at-your-own-risk on the kubelet with the `--experimental-allowed-unsafe-sysctls` flag. Future versions will improve on resource isolation and more sysctls will be considered safe.
```
This commit is contained in:
commit
4ddfc4849a
@ -183,6 +183,7 @@ func (s *KubeletServer) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.BoolVar(&s.MakeIPTablesUtilChains, "make-iptables-util-chains", s.MakeIPTablesUtilChains, "If true, kubelet will ensure iptables utility rules are present on host.")
|
||||
fs.Int32Var(&s.IPTablesMasqueradeBit, "iptables-masquerade-bit", s.IPTablesMasqueradeBit, "The bit of the fwmark space to mark packets for SNAT. Must be within the range [0, 31]. Please match this parameter with corresponding parameter in kube-proxy.")
|
||||
fs.Int32Var(&s.IPTablesDropBit, "iptables-drop-bit", s.IPTablesDropBit, "The bit of the fwmark space to mark packets for dropping. Must be within the range [0, 31].")
|
||||
fs.StringSliceVar(&s.AllowedUnsafeSysctls, "experimental-allowed-unsafe-sysctls", s.AllowedUnsafeSysctls, "Comma-separated whitelist of unsafe sysctls or unsafe sysctl patterns (ending in *). Use these at your own risk.")
|
||||
|
||||
// Flags intended for testing, not recommended used in production environments.
|
||||
fs.StringVar(&s.RemoteRuntimeEndpoint, "container-runtime-endpoint", s.RemoteRuntimeEndpoint, "The unix socket endpoint of remote runtime service. If not empty, this option will override --container-runtime. This is an experimental feature. Intended for testing only.")
|
||||
|
@ -290,6 +290,7 @@ func UnsecuredKubeletConfig(s *options.KubeletServer) (*KubeletConfig, error) {
|
||||
StandaloneMode: (len(s.APIServerList) == 0),
|
||||
StreamingConnectionIdleTimeout: s.StreamingConnectionIdleTimeout.Duration,
|
||||
SyncFrequency: s.SyncFrequency.Duration,
|
||||
AllowedUnsafeSysctls: s.AllowedUnsafeSysctls,
|
||||
SystemCgroups: s.SystemCgroups,
|
||||
TLSOptions: tlsOptions,
|
||||
Writer: writer,
|
||||
@ -1098,6 +1099,7 @@ type KubeletConfig struct {
|
||||
StandaloneMode bool
|
||||
StreamingConnectionIdleTimeout time.Duration
|
||||
SyncFrequency time.Duration
|
||||
AllowedUnsafeSysctls []string
|
||||
SystemCgroups string
|
||||
TLSOptions *server.TLSOptions
|
||||
Writer io.Writer
|
||||
@ -1218,6 +1220,7 @@ func CreateAndInitKubelet(kc *KubeletConfig) (k KubeletBootstrap, pc *config.Pod
|
||||
kc.MakeIPTablesUtilChains,
|
||||
kc.iptablesMasqueradeBit,
|
||||
kc.iptablesDropBit,
|
||||
kc.AllowedUnsafeSysctls,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
@ -100,6 +100,7 @@ pkg/kubelet/api
|
||||
pkg/kubelet/container
|
||||
pkg/kubelet/envvars
|
||||
pkg/kubelet/eviction
|
||||
pkg/kubelet/sysctls
|
||||
pkg/kubelet/util/format
|
||||
pkg/kubelet/util/ioutils
|
||||
pkg/kubelet/volume
|
||||
@ -139,6 +140,7 @@ pkg/runtime/serializer/yaml
|
||||
pkg/security
|
||||
pkg/security/podsecuritypolicy/apparmor
|
||||
pkg/security/podsecuritypolicy/capabilities
|
||||
pkg/security/podsecuritypolicy/sysctl
|
||||
pkg/serviceaccount
|
||||
pkg/storage
|
||||
pkg/storage/etcd3
|
||||
|
@ -164,6 +164,7 @@ executor-logv
|
||||
executor-path
|
||||
executor-suicide-timeout
|
||||
exit-on-lock-contention
|
||||
experimental-allowed-unsafe-sysctls
|
||||
experimental-bootstrap-kubeconfig
|
||||
experimental-flannel-overlay
|
||||
experimental-keystone-url
|
||||
|
@ -438,6 +438,20 @@ const (
|
||||
// PreferAvoidPodsAnnotationKey represents the key of preferAvoidPods data (json serialized)
|
||||
// in the Annotations of a Node.
|
||||
PreferAvoidPodsAnnotationKey string = "scheduler.alpha.kubernetes.io/preferAvoidPods"
|
||||
|
||||
// SysctlsPodAnnotationKey represents the key of sysctls which are set for the infrastructure
|
||||
// container of a pod. The annotation value is a comma separated list of sysctl_name=value
|
||||
// key-value pairs. Only a limited set of whitelisted and isolated sysctls is supported by
|
||||
// the kubelet. Pods with other sysctls will fail to launch.
|
||||
SysctlsPodAnnotationKey string = "security.alpha.kubernetes.io/sysctls"
|
||||
|
||||
// UnsafeSysctlsPodAnnotationKey represents the key of sysctls which are set for the infrastructure
|
||||
// container of a pod. The annotation value is a comma separated list of sysctl_name=value
|
||||
// key-value pairs. Unsafe sysctls must be explicitly enabled for a kubelet. They are properly
|
||||
// namespaced to a pod or a container, but their isolation is usually unclear or weak. Their use
|
||||
// is at-your-own-risk. Pods that attempt to set an unsafe sysctl that is not enabled for a kubelet
|
||||
// will fail to launch.
|
||||
UnsafeSysctlsPodAnnotationKey string = "security.alpha.kubernetes.io/unsafe-sysctls"
|
||||
)
|
||||
|
||||
// GetAffinityFromPod gets the json serialized affinity data from Pod.Annotations
|
||||
@ -522,3 +536,51 @@ func GetAvoidPodsFromNodeAnnotations(annotations map[string]string) (AvoidPods,
|
||||
}
|
||||
return avoidPods, nil
|
||||
}
|
||||
|
||||
// SysctlsFromPodAnnotations parses the sysctl annotations into a slice of safe Sysctls
|
||||
// and a slice of unsafe Sysctls. This is only a convenience wrapper around
|
||||
// SysctlsFromPodAnnotation.
|
||||
func SysctlsFromPodAnnotations(a map[string]string) ([]Sysctl, []Sysctl, error) {
|
||||
safe, err := SysctlsFromPodAnnotation(a[SysctlsPodAnnotationKey])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
unsafe, err := SysctlsFromPodAnnotation(a[UnsafeSysctlsPodAnnotationKey])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return safe, unsafe, nil
|
||||
}
|
||||
|
||||
// SysctlsFromPodAnnotation parses an annotation value into a slice of Sysctls.
|
||||
func SysctlsFromPodAnnotation(annotation string) ([]Sysctl, error) {
|
||||
if len(annotation) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
kvs := strings.Split(annotation, ",")
|
||||
sysctls := make([]Sysctl, len(kvs))
|
||||
for i, kv := range kvs {
|
||||
cs := strings.Split(kv, "=")
|
||||
if len(cs) != 2 {
|
||||
return nil, fmt.Errorf("sysctl %q not of the format sysctl_name=value", kv)
|
||||
}
|
||||
sysctls[i].Name = cs[0]
|
||||
sysctls[i].Value = cs[1]
|
||||
}
|
||||
return sysctls, nil
|
||||
}
|
||||
|
||||
// PodAnnotationsFromSysctls creates an annotation value for a slice of Sysctls.
|
||||
func PodAnnotationsFromSysctls(sysctls []Sysctl) string {
|
||||
if len(sysctls) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
kvs := make([]string, len(sysctls))
|
||||
for i := range sysctls {
|
||||
kvs[i] = fmt.Sprintf("%s=%s", sysctls[i].Name, sysctls[i].Value)
|
||||
}
|
||||
return strings.Join(kvs, ",")
|
||||
}
|
||||
|
@ -391,3 +391,42 @@ func TestGetAvoidPodsFromNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSysctlsFromPodAnnotation(t *testing.T) {
|
||||
type Test struct {
|
||||
annotation string
|
||||
expectValue []Sysctl
|
||||
expectErr bool
|
||||
}
|
||||
for i, test := range []Test{
|
||||
{
|
||||
annotation: "",
|
||||
expectValue: nil,
|
||||
},
|
||||
{
|
||||
annotation: "foo.bar",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
annotation: "foo.bar=42",
|
||||
expectValue: []Sysctl{{Name: "foo.bar", Value: "42"}},
|
||||
},
|
||||
{
|
||||
annotation: "foo.bar=42,",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
annotation: "foo.bar=42,abc.def=1",
|
||||
expectValue: []Sysctl{{Name: "foo.bar", Value: "42"}, {Name: "abc.def", Value: "1"}},
|
||||
},
|
||||
} {
|
||||
sysctls, err := SysctlsFromPodAnnotation(test.annotation)
|
||||
if test.expectErr && err == nil {
|
||||
t.Errorf("[%v]expected error but got none", i)
|
||||
} else if !test.expectErr && err != nil {
|
||||
t.Errorf("[%v]did not expect error but got: %v", i, err)
|
||||
} else if !reflect.DeepEqual(sysctls, test.expectValue) {
|
||||
t.Errorf("[%v]expect value %v but got %v", i, test.expectValue, sysctls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1564,6 +1564,14 @@ type PodSpec struct {
|
||||
Subdomain string `json:"subdomain,omitempty"`
|
||||
}
|
||||
|
||||
// Sysctl defines a kernel parameter to be set
|
||||
type Sysctl struct {
|
||||
// Name of a property to set
|
||||
Name string `json:"name"`
|
||||
// Value of a property to set
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// PodSecurityContext holds pod-level security attributes and common container settings.
|
||||
// Some fields are also present in container.securityContext. Field values of
|
||||
// container.securityContext take precedence over field values of PodSecurityContext.
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
@ -132,6 +133,23 @@ func ValidatePodSpecificAnnotations(annotations map[string]string, spec *api.Pod
|
||||
allErrs = append(allErrs, ValidateSeccompPodAnnotations(annotations, fldPath)...)
|
||||
allErrs = append(allErrs, ValidateAppArmorPodAnnotations(annotations, spec, fldPath)...)
|
||||
|
||||
sysctls, err := api.SysctlsFromPodAnnotation(annotations[api.SysctlsPodAnnotationKey])
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Key(api.SysctlsPodAnnotationKey), annotations[api.SysctlsPodAnnotationKey], err.Error()))
|
||||
} else {
|
||||
allErrs = append(allErrs, validateSysctls(sysctls, fldPath.Key(api.SysctlsPodAnnotationKey))...)
|
||||
}
|
||||
unsafeSysctls, err := api.SysctlsFromPodAnnotation(annotations[api.UnsafeSysctlsPodAnnotationKey])
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Key(api.UnsafeSysctlsPodAnnotationKey), annotations[api.UnsafeSysctlsPodAnnotationKey], err.Error()))
|
||||
} else {
|
||||
allErrs = append(allErrs, validateSysctls(unsafeSysctls, fldPath.Key(api.UnsafeSysctlsPodAnnotationKey))...)
|
||||
}
|
||||
inBoth := sysctlIntersection(sysctls, unsafeSysctls)
|
||||
if len(inBoth) > 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Key(api.UnsafeSysctlsPodAnnotationKey), strings.Join(inBoth, ", "), "can not be safe and unsafe"))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
@ -2128,6 +2146,40 @@ func podSpecHasContainer(spec *api.PodSpec, containerName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
// a sysctl segment regex, concatenated with dots to form a sysctl name
|
||||
SysctlSegmentFmt string = "[a-z0-9]([-_a-z0-9]*[a-z0-9])?"
|
||||
|
||||
// a sysctl name regex
|
||||
SysctlFmt string = "(" + SysctlSegmentFmt + "\\.)*" + SysctlSegmentFmt
|
||||
|
||||
// the maximal length of a sysctl name
|
||||
SysctlMaxLength int = 253
|
||||
)
|
||||
|
||||
var sysctlRegexp = regexp.MustCompile("^" + SysctlFmt + "$")
|
||||
|
||||
// IsValidSysctlName checks that the given string is a valid sysctl name,
|
||||
// i.e. matches SysctlFmt.
|
||||
func IsValidSysctlName(name string) bool {
|
||||
if len(name) > SysctlMaxLength {
|
||||
return false
|
||||
}
|
||||
return sysctlRegexp.MatchString(name)
|
||||
}
|
||||
|
||||
func validateSysctls(sysctls []api.Sysctl, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
for i, s := range sysctls {
|
||||
if len(s.Name) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("name"), ""))
|
||||
} else if !IsValidSysctlName(s.Name) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Index(i).Child("name"), s.Name, fmt.Sprintf("must have at most %d characters and match regex %s", SysctlMaxLength, SysctlFmt)))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidatePodSecurityContext test that the specified PodSecurityContext has valid data.
|
||||
func ValidatePodSecurityContext(securityContext *api.PodSecurityContext, spec *api.PodSpec, specPath, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
@ -3475,3 +3527,17 @@ func isValidHostnamesMap(serializedPodHostNames string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sysctlIntersection(a []api.Sysctl, b []api.Sysctl) []string {
|
||||
lookup := make(map[string]struct{}, len(a))
|
||||
result := []string{}
|
||||
for i := range a {
|
||||
lookup[a[i].Name] = struct{}{}
|
||||
}
|
||||
for i := range b {
|
||||
if _, found := lookup[b[i].Name]; found {
|
||||
result = append(result, b[i].Name)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -3538,6 +3538,17 @@ func TestValidatePod(t *testing.T) {
|
||||
},
|
||||
Spec: validPodSpec,
|
||||
},
|
||||
{ // syntactically valid sysctls
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.SysctlsPodAnnotationKey: "kernel.shmmni=32768,kernel.shmmax=1000000000",
|
||||
api.UnsafeSysctlsPodAnnotationKey: "knet.ipv4.route.min_pmtu=1000",
|
||||
},
|
||||
},
|
||||
Spec: validPodSpec,
|
||||
},
|
||||
}
|
||||
for _, pod := range successCases {
|
||||
if errs := ValidatePod(&pod); len(errs) != 0 {
|
||||
@ -3987,6 +3998,47 @@ func TestValidatePod(t *testing.T) {
|
||||
},
|
||||
Spec: validPodSpec,
|
||||
},
|
||||
"invalid sysctl annotation": {
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.SysctlsPodAnnotationKey: "foo:",
|
||||
},
|
||||
},
|
||||
Spec: validPodSpec,
|
||||
},
|
||||
"invalid comma-separated sysctl annotation": {
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.SysctlsPodAnnotationKey: "kernel.msgmax,",
|
||||
},
|
||||
},
|
||||
Spec: validPodSpec,
|
||||
},
|
||||
"invalid unsafe sysctl annotation": {
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.SysctlsPodAnnotationKey: "foo:",
|
||||
},
|
||||
},
|
||||
Spec: validPodSpec,
|
||||
},
|
||||
"intersecting safe sysctls and unsafe sysctls annotations": {
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "123",
|
||||
Namespace: "ns",
|
||||
Annotations: map[string]string{
|
||||
api.SysctlsPodAnnotationKey: "kernel.shmmax=10000000",
|
||||
api.UnsafeSysctlsPodAnnotationKey: "kernel.shmmax=10000000",
|
||||
},
|
||||
},
|
||||
Spec: validPodSpec,
|
||||
},
|
||||
}
|
||||
for k, v := range errorCases {
|
||||
if errs := ValidatePod(&v); len(errs) == 0 {
|
||||
@ -7826,3 +7878,91 @@ func TestValidateHasLabel(t *testing.T) {
|
||||
t.Errorf("expected failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidSysctlName(t *testing.T) {
|
||||
valid := []string{
|
||||
"a.b.c.d",
|
||||
"a",
|
||||
"a_b",
|
||||
"a-b",
|
||||
"abc",
|
||||
"abc.def",
|
||||
}
|
||||
invalid := []string{
|
||||
"",
|
||||
"*",
|
||||
"ä",
|
||||
"a_",
|
||||
"_",
|
||||
"__",
|
||||
"_a",
|
||||
"_a._b",
|
||||
"-",
|
||||
".",
|
||||
"a.",
|
||||
".a",
|
||||
"a.b.",
|
||||
"a*.b",
|
||||
"a*b",
|
||||
"*a",
|
||||
"a.*",
|
||||
"*",
|
||||
"abc*",
|
||||
"a.abc*",
|
||||
"a.b.*",
|
||||
"Abc",
|
||||
func(n int) string {
|
||||
x := make([]byte, n)
|
||||
for i := range x {
|
||||
x[i] = byte('a')
|
||||
}
|
||||
return string(x)
|
||||
}(256),
|
||||
}
|
||||
for _, s := range valid {
|
||||
if !IsValidSysctlName(s) {
|
||||
t.Errorf("%q expected to be a valid sysctl name", s)
|
||||
}
|
||||
}
|
||||
for _, s := range invalid {
|
||||
if IsValidSysctlName(s) {
|
||||
t.Errorf("%q expected to be an invalid sysctl name", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSysctls(t *testing.T) {
|
||||
valid := []string{
|
||||
"net.foo.bar",
|
||||
"kernel.shmmax",
|
||||
}
|
||||
invalid := []string{
|
||||
"i..nvalid",
|
||||
"_invalid",
|
||||
}
|
||||
|
||||
sysctls := make([]api.Sysctl, len(valid))
|
||||
for i, sysctl := range valid {
|
||||
sysctls[i].Name = sysctl
|
||||
}
|
||||
errs := validateSysctls(sysctls, field.NewPath("foo"))
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("unexpected validation errors: %v", errs)
|
||||
}
|
||||
|
||||
sysctls = make([]api.Sysctl, len(invalid))
|
||||
for i, sysctl := range invalid {
|
||||
sysctls[i].Name = sysctl
|
||||
}
|
||||
errs = validateSysctls(sysctls, field.NewPath("foo"))
|
||||
if len(errs) != 2 {
|
||||
t.Errorf("expected 2 validation errors. Got: %v", errs)
|
||||
} else {
|
||||
if got, expected := errs[0].Error(), "foo"; !strings.Contains(got, expected) {
|
||||
t.Errorf("unexpected errors: expected=%q, got=%q", expected, got)
|
||||
}
|
||||
if got, expected := errs[1].Error(), "foo"; !strings.Contains(got, expected) {
|
||||
t.Errorf("unexpected errors: expected=%q, got=%q", expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -186,6 +186,7 @@ func RegisterDeepCopies(scheme *runtime.Scheme) error {
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_api_ServiceProxyOptions, InType: reflect.TypeOf(&ServiceProxyOptions{})},
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_api_ServiceSpec, InType: reflect.TypeOf(&ServiceSpec{})},
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_api_ServiceStatus, InType: reflect.TypeOf(&ServiceStatus{})},
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_api_Sysctl, InType: reflect.TypeOf(&Sysctl{})},
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_api_TCPSocketAction, InType: reflect.TypeOf(&TCPSocketAction{})},
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_api_Taint, InType: reflect.TypeOf(&Taint{})},
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_api_Toleration, InType: reflect.TypeOf(&Toleration{})},
|
||||
@ -3481,6 +3482,16 @@ func DeepCopy_api_ServiceStatus(in interface{}, out interface{}, c *conversion.C
|
||||
}
|
||||
}
|
||||
|
||||
func DeepCopy_api_Sysctl(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*Sysctl)
|
||||
out := out.(*Sysctl)
|
||||
out.Name = in.Name
|
||||
out.Value = in.Value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeepCopy_api_TCPSocketAction(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*TCPSocketAction)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -419,6 +419,8 @@ type KubeletConfiguration struct {
|
||||
// iptablesDropBit is the bit of the iptables fwmark space to use for dropping packets. Kubelet will ensure iptables mark and drop rules.
|
||||
// Values must be within the range [0, 31]. Must be different from IPTablesMasqueradeBit
|
||||
IPTablesDropBit int32 `json:"iptablesDropBit"`
|
||||
// Whitelist of unsafe sysctls or sysctl patterns (ending in *).
|
||||
AllowedUnsafeSysctls []string `json:"experimentalAllowedUnsafeSysctls,omitempty"`
|
||||
}
|
||||
|
||||
type KubeSchedulerConfiguration struct {
|
||||
|
@ -474,4 +474,7 @@ type KubeletConfiguration struct {
|
||||
// iptablesDropBit is the bit of the iptables fwmark space to mark for dropping packets.
|
||||
// Values must be within the range [0, 31]. Must be different from other mark bits.
|
||||
IPTablesDropBit *int32 `json:"iptablesDropBit"`
|
||||
// Whitelist of unsafe sysctls or sysctl patterns (ending in *). Use these at your own risk.
|
||||
// Resource isolation might be lacking and pod might influence each other on the same node.
|
||||
AllowedUnsafeSysctls []string `json:"allowedUnsafeSysctls,omitempty"`
|
||||
}
|
||||
|
@ -332,6 +332,7 @@ func autoConvert_v1alpha1_KubeletConfiguration_To_componentconfig_KubeletConfigu
|
||||
if err := api.Convert_Pointer_int32_To_int32(&in.IPTablesDropBit, &out.IPTablesDropBit, s); err != nil {
|
||||
return err
|
||||
}
|
||||
out.AllowedUnsafeSysctls = in.AllowedUnsafeSysctls
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -509,6 +510,7 @@ func autoConvert_componentconfig_KubeletConfiguration_To_v1alpha1_KubeletConfigu
|
||||
if err := api.Convert_int32_To_Pointer_int32(&in.IPTablesDropBit, &out.IPTablesDropBit, s); err != nil {
|
||||
return err
|
||||
}
|
||||
out.AllowedUnsafeSysctls = in.AllowedUnsafeSysctls
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -402,6 +402,13 @@ func DeepCopy_v1alpha1_KubeletConfiguration(in interface{}, out interface{}, c *
|
||||
} else {
|
||||
out.IPTablesDropBit = nil
|
||||
}
|
||||
if in.AllowedUnsafeSysctls != nil {
|
||||
in, out := &in.AllowedUnsafeSysctls, &out.AllowedUnsafeSysctls
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
} else {
|
||||
out.AllowedUnsafeSysctls = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -337,6 +337,13 @@ func DeepCopy_componentconfig_KubeletConfiguration(in interface{}, out interface
|
||||
out.MakeIPTablesUtilChains = in.MakeIPTablesUtilChains
|
||||
out.IPTablesMasqueradeBit = in.IPTablesMasqueradeBit
|
||||
out.IPTablesDropBit = in.IPTablesDropBit
|
||||
if in.AllowedUnsafeSysctls != nil {
|
||||
in, out := &in.AllowedUnsafeSysctls, &out.AllowedUnsafeSysctls
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
} else {
|
||||
out.AllowedUnsafeSysctls = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
37
pkg/apis/extensions/helpers.go
Normal file
37
pkg/apis/extensions/helpers.go
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SysctlsFromPodSecurityPolicyAnnotation parses an annotation value of the key
|
||||
// SysctlsSecurityPolocyAnnotationKey into a slice of sysctls. An empty slice
|
||||
// is returned if annotation is the empty string.
|
||||
func SysctlsFromPodSecurityPolicyAnnotation(annotation string) ([]string, error) {
|
||||
if len(annotation) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return strings.Split(annotation, ","), nil
|
||||
}
|
||||
|
||||
// PodAnnotationsFromSysctls creates an annotation value for a slice of Sysctls.
|
||||
func PodAnnotationsFromSysctls(sysctls []string) string {
|
||||
return strings.Join(sysctls, ",")
|
||||
}
|
62
pkg/apis/extensions/helpers_test.go
Normal file
62
pkg/apis/extensions/helpers_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPodAnnotationsFromSysctls(t *testing.T) {
|
||||
type Test struct {
|
||||
sysctls []string
|
||||
expectedValue string
|
||||
}
|
||||
for _, test := range []Test{
|
||||
{sysctls: []string{"a.b"}, expectedValue: "a.b"},
|
||||
{sysctls: []string{"a.b", "c.d"}, expectedValue: "a.b,c.d"},
|
||||
{sysctls: []string{"a.b", "a.b"}, expectedValue: "a.b,a.b"},
|
||||
{sysctls: []string{}, expectedValue: ""},
|
||||
{sysctls: nil, expectedValue: ""},
|
||||
} {
|
||||
a := PodAnnotationsFromSysctls(test.sysctls)
|
||||
if a != test.expectedValue {
|
||||
t.Errorf("wrong value for %v: got=%q wanted=%q", test.sysctls, a, test.expectedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSysctlsFromPodSecurityPolicyAnnotation(t *testing.T) {
|
||||
type Test struct {
|
||||
expectedValue []string
|
||||
annotation string
|
||||
}
|
||||
for _, test := range []Test{
|
||||
{annotation: "a.b", expectedValue: []string{"a.b"}},
|
||||
{annotation: "a.b,c.d", expectedValue: []string{"a.b", "c.d"}},
|
||||
{annotation: "a.b,a.b", expectedValue: []string{"a.b", "a.b"}},
|
||||
{annotation: "", expectedValue: []string{}},
|
||||
} {
|
||||
sysctls, err := SysctlsFromPodSecurityPolicyAnnotation(test.annotation)
|
||||
if err != nil {
|
||||
t.Errorf("error for %q: %v", test.annotation, err)
|
||||
}
|
||||
if !reflect.DeepEqual(sysctls, test.expectedValue) {
|
||||
t.Errorf("wrong value for %q: got=%v wanted=%v", test.annotation, sysctls, test.expectedValue)
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,13 @@ import (
|
||||
"k8s.io/kubernetes/pkg/util/intstr"
|
||||
)
|
||||
|
||||
const (
|
||||
// SysctlsPodSecurityPolicyAnnotationKey represents the key of a whitelist of
|
||||
// allowed safe and unsafe sysctls in a pod spec. It's a comma-separated list of plain sysctl
|
||||
// names or sysctl patterns (which end in *). The string "*" matches all sysctls.
|
||||
SysctlsPodSecurityPolicyAnnotationKey string = "security.alpha.kubernetes.io/sysctls"
|
||||
)
|
||||
|
||||
// describes the attributes of a scale subresource
|
||||
type ScaleSpec struct {
|
||||
// desired number of instances for the scaled object.
|
||||
|
@ -574,6 +574,7 @@ func ValidatePodSecurityPolicySpec(spec *extensions.PodSecurityPolicySpec, fldPa
|
||||
|
||||
func ValidatePodSecurityPolicySpecificAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if p := annotations[apparmor.DefaultProfileAnnotationKey]; p != "" {
|
||||
if err := apparmor.ValidateProfileFormat(p); err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Key(apparmor.DefaultProfileAnnotationKey), p, err.Error()))
|
||||
@ -586,6 +587,16 @@ func ValidatePodSecurityPolicySpecificAnnotations(annotations map[string]string,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sysctlAnnotation := annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey]
|
||||
sysctlFldPath := fldPath.Key(extensions.SysctlsPodSecurityPolicyAnnotationKey)
|
||||
sysctls, err := extensions.SysctlsFromPodSecurityPolicyAnnotation(sysctlAnnotation)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(sysctlFldPath, sysctlAnnotation, err.Error()))
|
||||
} else {
|
||||
allErrs = append(allErrs, validatePodSecurityPolicySysctls(sysctlFldPath, sysctls)...)
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
@ -674,6 +685,36 @@ func validatePodSecurityPolicyVolumes(fldPath *field.Path, volumes []extensions.
|
||||
return allErrs
|
||||
}
|
||||
|
||||
const sysctlPatternSegmentFmt string = "([a-z0-9][-_a-z0-9]*)?[a-z0-9*]"
|
||||
const SysctlPatternFmt string = "(" + apivalidation.SysctlSegmentFmt + "\\.)*" + sysctlPatternSegmentFmt
|
||||
|
||||
var sysctlPatternRegexp = regexp.MustCompile("^" + SysctlPatternFmt + "$")
|
||||
|
||||
func IsValidSysctlPattern(name string) bool {
|
||||
if len(name) > apivalidation.SysctlMaxLength {
|
||||
return false
|
||||
}
|
||||
return sysctlPatternRegexp.MatchString(name)
|
||||
}
|
||||
|
||||
// validatePodSecurityPolicySysctls validates the sysctls fields of PodSecurityPolicy.
|
||||
func validatePodSecurityPolicySysctls(fldPath *field.Path, sysctls []string) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
for i, s := range sysctls {
|
||||
if !IsValidSysctlPattern(string(s)) {
|
||||
allErrs = append(
|
||||
allErrs,
|
||||
field.Invalid(fldPath.Index(i), sysctls[i], fmt.Sprintf("must have at most %d characters and match regex %s",
|
||||
apivalidation.SysctlMaxLength,
|
||||
SysctlPatternFmt,
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validateIDRanges ensures the range is valid.
|
||||
func validateIDRanges(fldPath *field.Path, rng extensions.IDRange) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
@ -1512,7 +1512,8 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
||||
validPSP := func() *extensions.PodSecurityPolicy {
|
||||
return &extensions.PodSecurityPolicy{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Name: "foo",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: extensions.PodSecurityPolicySpec{
|
||||
SELinux: extensions.SELinuxStrategyOptions{
|
||||
@ -1596,6 +1597,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + ",not-good",
|
||||
}
|
||||
|
||||
invalidSysctlPattern := validPSP()
|
||||
invalidSysctlPattern.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "a.*.b"
|
||||
|
||||
errorCases := map[string]struct {
|
||||
psp *extensions.PodSecurityPolicy
|
||||
errorType field.ErrorType
|
||||
@ -1686,6 +1690,11 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
||||
errorType: field.ErrorTypeInvalid,
|
||||
errorDetail: "invalid AppArmor profile name: \"not-good\"",
|
||||
},
|
||||
"invalid sysctl pattern": {
|
||||
psp: invalidSysctlPattern,
|
||||
errorType: field.ErrorTypeInvalid,
|
||||
errorDetail: fmt.Sprintf("must have at most 253 characters and match regex %s", SysctlPatternFmt),
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range errorCases {
|
||||
@ -1728,6 +1737,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
||||
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," + apparmor.ProfileNamePrefix + "foo",
|
||||
}
|
||||
|
||||
withSysctl := validPSP()
|
||||
withSysctl.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "net.*"
|
||||
|
||||
successCases := map[string]struct {
|
||||
psp *extensions.PodSecurityPolicy
|
||||
}{
|
||||
@ -1749,6 +1761,9 @@ func TestValidatePodSecurityPolicy(t *testing.T) {
|
||||
"valid AppArmor annotations": {
|
||||
psp: validAppArmor,
|
||||
},
|
||||
"with network sysctls": {
|
||||
psp: withSysctl,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range successCases {
|
||||
@ -2031,6 +2046,58 @@ func TestValidateNetworkPolicyUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidSysctlPattern(t *testing.T) {
|
||||
valid := []string{
|
||||
"a.b.c.d",
|
||||
"a",
|
||||
"a_b",
|
||||
"a-b",
|
||||
"abc",
|
||||
"abc.def",
|
||||
"*",
|
||||
"a.*",
|
||||
"*",
|
||||
"abc*",
|
||||
"a.abc*",
|
||||
"a.b.*",
|
||||
}
|
||||
invalid := []string{
|
||||
"",
|
||||
"ä",
|
||||
"a_",
|
||||
"_",
|
||||
"_a",
|
||||
"_a._b",
|
||||
"__",
|
||||
"-",
|
||||
".",
|
||||
"a.",
|
||||
".a",
|
||||
"a.b.",
|
||||
"a*.b",
|
||||
"a*b",
|
||||
"*a",
|
||||
"Abc",
|
||||
func(n int) string {
|
||||
x := make([]byte, n)
|
||||
for i := range x {
|
||||
x[i] = byte('a')
|
||||
}
|
||||
return string(x)
|
||||
}(256),
|
||||
}
|
||||
for _, s := range valid {
|
||||
if !IsValidSysctlPattern(s) {
|
||||
t.Errorf("%q expected to be a valid sysctl pattern", s)
|
||||
}
|
||||
}
|
||||
for _, s := range invalid {
|
||||
if IsValidSysctlPattern(s) {
|
||||
t.Errorf("%q expected to be an invalid sysctl pattern", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newBool(val bool) *bool {
|
||||
p := new(bool)
|
||||
*p = val
|
||||
|
@ -76,9 +76,10 @@ const (
|
||||
// docker version should be at least 1.9.x
|
||||
minimumDockerAPIVersion = "1.21"
|
||||
|
||||
// Remote API version for docker daemon version v1.10
|
||||
// Remote API version for docker daemon versions
|
||||
// https://docs.docker.com/engine/reference/api/docker_remote_api/
|
||||
dockerV110APIVersion = "1.22"
|
||||
DockerV112APIVersion = "1.24"
|
||||
|
||||
// ndots specifies the minimum number of dots that a domain name must contain for the resolver to consider it as FQDN (fully-qualified)
|
||||
// we want to able to consider SRV lookup names like _dns._udp.kube-dns.default.svc to be considered relative.
|
||||
@ -660,6 +661,22 @@ func (dm *DockerManager) runContainer(
|
||||
SecurityOpt: securityOpts,
|
||||
}
|
||||
|
||||
// Set sysctls if requested
|
||||
sysctls, unsafeSysctls, err := api.SysctlsFromPodAnnotations(pod.Annotations)
|
||||
if err != nil {
|
||||
dm.recorder.Eventf(ref, api.EventTypeWarning, events.FailedToCreateContainer, "Failed to create docker container %q of pod %q with error: %v", container.Name, format.Pod(pod), err)
|
||||
return kubecontainer.ContainerID{}, err
|
||||
}
|
||||
if len(sysctls)+len(unsafeSysctls) > 0 {
|
||||
hc.Sysctls = make(map[string]string, len(sysctls)+len(unsafeSysctls))
|
||||
for _, c := range sysctls {
|
||||
hc.Sysctls[c.Name] = c.Value
|
||||
}
|
||||
for _, c := range unsafeSysctls {
|
||||
hc.Sysctls[c.Name] = c.Value
|
||||
}
|
||||
}
|
||||
|
||||
// If current api version is newer than docker 1.10 requested, set OomScoreAdj to HostConfig
|
||||
result, err := dm.checkDockerAPIVersion(dockerV110APIVersion)
|
||||
if err != nil {
|
||||
|
@ -67,6 +67,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/kubelet/server"
|
||||
"k8s.io/kubernetes/pkg/kubelet/server/stats"
|
||||
"k8s.io/kubernetes/pkg/kubelet/status"
|
||||
"k8s.io/kubernetes/pkg/kubelet/sysctl"
|
||||
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
|
||||
"k8s.io/kubernetes/pkg/kubelet/util/format"
|
||||
"k8s.io/kubernetes/pkg/kubelet/util/ioutils"
|
||||
@ -249,6 +250,7 @@ func NewMainKubelet(
|
||||
makeIPTablesUtilChains bool,
|
||||
iptablesMasqueradeBit int,
|
||||
iptablesDropBit int,
|
||||
allowedUnsafeSysctls []string,
|
||||
) (*Kubelet, error) {
|
||||
if rootDirectory == "" {
|
||||
return nil, fmt.Errorf("invalid root directory %q", rootDirectory)
|
||||
@ -591,6 +593,26 @@ func NewMainKubelet(
|
||||
klet.evictionManager = evictionManager
|
||||
klet.AddPodAdmitHandler(evictionAdmitHandler)
|
||||
|
||||
// add sysctl admission
|
||||
runtimeSupport, err := sysctl.NewRuntimeAdmitHandler(klet.containerRuntime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
safeWhitelist, err := sysctl.NewWhitelist(sysctl.SafeSysctlWhitelist(), api.SysctlsPodAnnotationKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Safe, whitelisted sysctls can always be used as unsafe sysctls in the spec
|
||||
// Hence, we concatenate those two lists.
|
||||
safeAndUnsafeSysctls := append(sysctl.SafeSysctlWhitelist(), allowedUnsafeSysctls...)
|
||||
unsafeWhitelist, err := sysctl.NewWhitelist(safeAndUnsafeSysctls, api.UnsafeSysctlsPodAnnotationKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
klet.AddPodAdmitHandler(runtimeSupport)
|
||||
klet.AddPodAdmitHandler(safeWhitelist)
|
||||
klet.AddPodAdmitHandler(unsafeWhitelist)
|
||||
|
||||
// enable active deadline handler
|
||||
activeDeadlineHandler, err := newActiveDeadlineHandler(klet.statusManager, klet.recorder, klet.clock)
|
||||
if err != nil {
|
||||
|
60
pkg/kubelet/sysctl/namespace.go
Normal file
60
pkg/kubelet/sysctl/namespace.go
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Namespace represents a kernel namespace name.
|
||||
type Namespace string
|
||||
|
||||
const (
|
||||
// the Linux IPC namespace
|
||||
IpcNamespace = Namespace("ipc")
|
||||
|
||||
// the network namespace
|
||||
NetNamespace = Namespace("net")
|
||||
|
||||
// the zero value if no namespace is known
|
||||
UnknownNamespace = Namespace("")
|
||||
)
|
||||
|
||||
var namespaces = map[string]Namespace{
|
||||
"kernel.sem": IpcNamespace,
|
||||
}
|
||||
|
||||
var prefixNamespaces = map[string]Namespace{
|
||||
"kernel.shm": IpcNamespace,
|
||||
"kernel.msg": IpcNamespace,
|
||||
"fs.mqueue.": IpcNamespace,
|
||||
"net.": NetNamespace,
|
||||
}
|
||||
|
||||
// NamespacedBy returns the namespace of the Linux kernel for a sysctl, or
|
||||
// UnknownNamespace if the sysctl is not known to be namespaced.
|
||||
func NamespacedBy(val string) Namespace {
|
||||
if ns, found := namespaces[val]; found {
|
||||
return ns
|
||||
}
|
||||
for p, ns := range prefixNamespaces {
|
||||
if strings.HasPrefix(val, p) {
|
||||
return ns
|
||||
}
|
||||
}
|
||||
return UnknownNamespace
|
||||
}
|
36
pkg/kubelet/sysctl/namespace_test.go
Normal file
36
pkg/kubelet/sysctl/namespace_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNamespacedBy(t *testing.T) {
|
||||
tests := map[string]Namespace{
|
||||
"kernel.shm_rmid_forced": IpcNamespace,
|
||||
"net.a.b.c": NetNamespace,
|
||||
"fs.mqueue.a.b.c": IpcNamespace,
|
||||
"foo": UnknownNamespace,
|
||||
}
|
||||
|
||||
for sysctl, ns := range tests {
|
||||
if got := NamespacedBy(sysctl); got != ns {
|
||||
t.Errorf("wrong namespace for %q: got=%s want=%s", sysctl, got, ns)
|
||||
}
|
||||
}
|
||||
}
|
96
pkg/kubelet/sysctl/runtime.go
Normal file
96
pkg/kubelet/sysctl/runtime.go
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/kubelet/container"
|
||||
"k8s.io/kubernetes/pkg/kubelet/dockertools"
|
||||
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
|
||||
)
|
||||
|
||||
const (
|
||||
UnsupportedReason = "SysctlUnsupported"
|
||||
)
|
||||
|
||||
type runtimeAdmitHandler struct {
|
||||
result lifecycle.PodAdmitResult
|
||||
}
|
||||
|
||||
var _ lifecycle.PodAdmitHandler = &runtimeAdmitHandler{}
|
||||
|
||||
// NewRuntimeAdmitHandler returns a sysctlRuntimeAdmitHandler which checks whether
|
||||
// the given runtime support sysctls.
|
||||
func NewRuntimeAdmitHandler(runtime container.Runtime) (*runtimeAdmitHandler, error) {
|
||||
if runtime.Type() == dockertools.DockerType {
|
||||
v, err := runtime.APIVersion()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get runtime version: %v", err)
|
||||
}
|
||||
|
||||
// only Docker >= 1.12 supports sysctls
|
||||
c, err := v.Compare(dockertools.DockerV112APIVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compare Docker version for sysctl support: %v", err)
|
||||
}
|
||||
if c >= 0 {
|
||||
return &runtimeAdmitHandler{
|
||||
result: lifecycle.PodAdmitResult{
|
||||
Admit: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return &runtimeAdmitHandler{
|
||||
result: lifecycle.PodAdmitResult{
|
||||
Admit: false,
|
||||
Reason: UnsupportedReason,
|
||||
Message: "Docker before 1.12 does not support sysctls",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// for other runtimes like rkt sysctls are not supported
|
||||
return &runtimeAdmitHandler{
|
||||
result: lifecycle.PodAdmitResult{
|
||||
Admit: false,
|
||||
Reason: UnsupportedReason,
|
||||
Message: fmt.Sprintf("runtime %v does not support sysctls", runtime.Type()),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Admit checks whether the runtime supports sysctls.
|
||||
func (w *runtimeAdmitHandler) Admit(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
|
||||
sysctls, unsafeSysctls, err := api.SysctlsFromPodAnnotations(attrs.Pod.Annotations)
|
||||
if err != nil {
|
||||
return lifecycle.PodAdmitResult{
|
||||
Admit: false,
|
||||
Reason: AnnotationInvalidReason,
|
||||
Message: fmt.Sprintf("invalid sysctl annotation: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
if len(sysctls)+len(unsafeSysctls) > 0 {
|
||||
return w.result
|
||||
}
|
||||
|
||||
return lifecycle.PodAdmitResult{
|
||||
Admit: true,
|
||||
}
|
||||
}
|
171
pkg/kubelet/sysctl/whitelist.go
Normal file
171
pkg/kubelet/sysctl/whitelist.go
Normal file
@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/validation"
|
||||
extvalidation "k8s.io/kubernetes/pkg/apis/extensions/validation"
|
||||
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
|
||||
)
|
||||
|
||||
const (
|
||||
AnnotationInvalidReason = "InvalidSysctlAnnotation"
|
||||
ForbiddenReason = "SysctlForbidden"
|
||||
)
|
||||
|
||||
// SafeSysctlWhitelist returns the whitelist of safe sysctls and safe sysctl patterns (ending in *).
|
||||
//
|
||||
// A sysctl is called safe iff
|
||||
// - it is namespaced in the container or the pod
|
||||
// - it is isolated, i.e. has no influence on any other pod on the same node.
|
||||
func SafeSysctlWhitelist() []string {
|
||||
return []string{
|
||||
"kernel.shm_rmid_forced",
|
||||
"net.ipv4.ip_local_port_range",
|
||||
"net.ipv4.tcp_max_syn_backlog",
|
||||
"net.ipv4.tcp_syncookies",
|
||||
}
|
||||
}
|
||||
|
||||
// Whitelist provides a list of allowed sysctls and sysctl patterns (ending in *)
|
||||
// and a function to check whether a given sysctl matches this list.
|
||||
type Whitelist interface {
|
||||
// Validate checks that all sysctls given in a api.SysctlsPodAnnotationKey annotation
|
||||
// are valid according to the whitelist.
|
||||
Validate(pod *api.Pod) error
|
||||
}
|
||||
|
||||
// patternWhitelist takes a list of sysctls or sysctl patterns (ending in *) and
|
||||
// checks validity via a sysctl and prefix map, rejecting those which are not known
|
||||
// to be namespaced.
|
||||
type patternWhitelist struct {
|
||||
sysctls map[string]Namespace
|
||||
prefixes map[string]Namespace
|
||||
annotationKey string
|
||||
}
|
||||
|
||||
var _ lifecycle.PodAdmitHandler = &patternWhitelist{}
|
||||
|
||||
// NewWhitelist creates a new Whitelist from a list of sysctls and sysctl pattern (ending in *).
|
||||
func NewWhitelist(patterns []string, annotationKey string) (*patternWhitelist, error) {
|
||||
w := &patternWhitelist{
|
||||
sysctls: map[string]Namespace{},
|
||||
prefixes: map[string]Namespace{},
|
||||
annotationKey: annotationKey,
|
||||
}
|
||||
|
||||
for _, s := range patterns {
|
||||
if !extvalidation.IsValidSysctlPattern(s) {
|
||||
return nil, fmt.Errorf("sysctl %q must have at most %d characters and match regex %s",
|
||||
s,
|
||||
validation.SysctlMaxLength,
|
||||
extvalidation.SysctlPatternFmt,
|
||||
)
|
||||
}
|
||||
if strings.HasSuffix(s, "*") {
|
||||
prefix := s[:len(s)-1]
|
||||
ns := NamespacedBy(prefix)
|
||||
if ns == UnknownNamespace {
|
||||
return nil, fmt.Errorf("the sysctls %q are not known to be namespaced", s)
|
||||
}
|
||||
w.prefixes[prefix] = ns
|
||||
} else {
|
||||
ns := NamespacedBy(s)
|
||||
if ns == UnknownNamespace {
|
||||
return nil, fmt.Errorf("the sysctl %q are not known to be namespaced", s)
|
||||
}
|
||||
w.sysctls[s] = ns
|
||||
}
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// validateSysctl checks that a sysctl is whitelisted because it is known
|
||||
// to be namespaced by the Linux kernel. Note that being whitelisted is required, but not
|
||||
// sufficient: the container runtime might have a stricter check and refuse to launch a pod.
|
||||
//
|
||||
// The parameters hostNet and hostIPC are used to forbid sysctls for pod sharing the
|
||||
// respective namespaces with the host. This check is only possible for sysctls on
|
||||
// the static default whitelist, not those on the custom whitelist provided by the admin.
|
||||
func (w *patternWhitelist) validateSysctl(sysctl string, hostNet, hostIPC bool) error {
|
||||
nsErrorFmt := "%q not allowed with host %s enabled"
|
||||
if ns, found := w.sysctls[sysctl]; found {
|
||||
if ns == IpcNamespace && hostIPC {
|
||||
return fmt.Errorf(nsErrorFmt, sysctl, ns)
|
||||
}
|
||||
if ns == NetNamespace && hostNet {
|
||||
return fmt.Errorf(nsErrorFmt, sysctl, ns)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for p, ns := range w.prefixes {
|
||||
if strings.HasPrefix(sysctl, p) {
|
||||
if ns == IpcNamespace && hostIPC {
|
||||
return fmt.Errorf(nsErrorFmt, sysctl, ns)
|
||||
}
|
||||
if ns == NetNamespace && hostNet {
|
||||
return fmt.Errorf(nsErrorFmt, sysctl, ns)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%q not whitelisted", sysctl)
|
||||
}
|
||||
|
||||
// Admit checks that all sysctls given in a api.SysctlsPodAnnotationKey annotation
|
||||
// are valid according to the whitelist.
|
||||
func (w *patternWhitelist) Admit(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
|
||||
pod := attrs.Pod
|
||||
a := pod.Annotations[w.annotationKey]
|
||||
if a == "" {
|
||||
return lifecycle.PodAdmitResult{
|
||||
Admit: true,
|
||||
}
|
||||
}
|
||||
|
||||
sysctls, err := api.SysctlsFromPodAnnotation(a)
|
||||
if err != nil {
|
||||
return lifecycle.PodAdmitResult{
|
||||
Admit: false,
|
||||
Reason: AnnotationInvalidReason,
|
||||
Message: fmt.Sprintf("invalid %s annotation: %v", w.annotationKey, err),
|
||||
}
|
||||
}
|
||||
|
||||
var hostNet, hostIPC bool
|
||||
if pod.Spec.SecurityContext != nil {
|
||||
hostNet = pod.Spec.SecurityContext.HostNetwork
|
||||
hostIPC = pod.Spec.SecurityContext.HostIPC
|
||||
}
|
||||
for _, s := range sysctls {
|
||||
if err := w.validateSysctl(s.Name, hostNet, hostIPC); err != nil {
|
||||
return lifecycle.PodAdmitResult{
|
||||
Admit: false,
|
||||
Reason: ForbiddenReason,
|
||||
Message: fmt.Sprintf("forbidden sysctl: %v", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lifecycle.PodAdmitResult{
|
||||
Admit: true,
|
||||
}
|
||||
}
|
84
pkg/kubelet/sysctl/whitelist_test.go
Normal file
84
pkg/kubelet/sysctl/whitelist_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
)
|
||||
|
||||
func TestNewWhitelist(t *testing.T) {
|
||||
type Test struct {
|
||||
sysctls []string
|
||||
err bool
|
||||
}
|
||||
for _, test := range []Test{
|
||||
{sysctls: []string{"kernel.msg*", "kernel.sem"}},
|
||||
{sysctls: []string{" kernel.msg*"}, err: true},
|
||||
{sysctls: []string{"kernel.msg* "}, err: true},
|
||||
{sysctls: []string{"net.-"}, err: true},
|
||||
{sysctls: []string{"net.*.foo"}, err: true},
|
||||
{sysctls: []string{"foo"}, err: true},
|
||||
} {
|
||||
_, err := NewWhitelist(append(SafeSysctlWhitelist(), test.sysctls...), api.SysctlsPodAnnotationKey)
|
||||
if test.err && err == nil {
|
||||
t.Errorf("expected an error creating a whitelist for %v", test.sysctls)
|
||||
} else if !test.err && err != nil {
|
||||
t.Errorf("got unexpected error creating a whitelist for %v: %v", test.sysctls, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelist(t *testing.T) {
|
||||
type Test struct {
|
||||
sysctl string
|
||||
hostNet, hostIPC bool
|
||||
}
|
||||
valid := []Test{
|
||||
{sysctl: "kernel.shm_rmid_forced"},
|
||||
{sysctl: "net.ipv4.ip_local_port_range"},
|
||||
{sysctl: "kernel.msgmax"},
|
||||
{sysctl: "kernel.sem"},
|
||||
}
|
||||
invalid := []Test{
|
||||
{sysctl: "kernel.shm_rmid_forced", hostIPC: true},
|
||||
{sysctl: "net.ipv4.ip_local_port_range", hostNet: true},
|
||||
{sysctl: "foo"},
|
||||
{sysctl: "net.a.b.c", hostNet: false},
|
||||
{sysctl: "net.ipv4.ip_local_port_range.a.b.c", hostNet: false},
|
||||
{sysctl: "kernel.msgmax", hostIPC: true},
|
||||
{sysctl: "kernel.sem", hostIPC: true},
|
||||
}
|
||||
|
||||
w, err := NewWhitelist(append(SafeSysctlWhitelist(), "kernel.msg*", "kernel.sem"), api.SysctlsPodAnnotationKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create whitelist: %v", err)
|
||||
}
|
||||
|
||||
for _, test := range valid {
|
||||
if err := w.validateSysctl(test.sysctl, test.hostNet, test.hostIPC); err != nil {
|
||||
t.Errorf("expected to be whitelisted: %+v, got: %v", test, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, test := range invalid {
|
||||
if err := w.validateSysctl(test.sysctl, test.hostNet, test.hostIPC); err == nil {
|
||||
t.Errorf("expected to be rejected: %+v", test)
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/group"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/user"
|
||||
"k8s.io/kubernetes/pkg/util/errors"
|
||||
)
|
||||
@ -70,6 +71,19 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
var unsafeSysctls []string
|
||||
if ann, found := psp.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey]; found {
|
||||
var err error
|
||||
unsafeSysctls, err = extensions.SysctlsFromPodSecurityPolicyAnnotation(ann)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
sysctlsStrat, err := createSysctlsStrategy(unsafeSysctls)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.NewAggregate(errs)
|
||||
}
|
||||
@ -81,6 +95,7 @@ func (f *simpleStrategyFactory) CreateStrategies(psp *extensions.PodSecurityPoli
|
||||
FSGroupStrategy: fsGroupStrat,
|
||||
SupplementalGroupStrategy: supGroupStrat,
|
||||
CapabilitiesStrategy: capStrat,
|
||||
SysctlsStrategy: sysctlsStrat,
|
||||
}
|
||||
|
||||
return strategies, nil
|
||||
@ -145,3 +160,8 @@ func createSupplementalGroupStrategy(opts *extensions.SupplementalGroupsStrategy
|
||||
func createCapabilitiesStrategy(defaultAddCaps, requiredDropCaps, allowedCaps []api.Capability) (capabilities.Strategy, error) {
|
||||
return capabilities.NewDefaultCapabilities(defaultAddCaps, requiredDropCaps, allowedCaps)
|
||||
}
|
||||
|
||||
// createSysctlsStrategy creates a new unsafe sysctls strategy.
|
||||
func createSysctlsStrategy(sysctlsPatterns []string) (sysctl.SysctlsStrategy, error) {
|
||||
return sysctl.NewMustMatchPatterns(sysctlsPatterns)
|
||||
}
|
||||
|
@ -210,6 +210,8 @@ func (s *simpleProvider) ValidatePodSecurityContext(pod *api.Pod, fldPath *field
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("hostIPC"), pod.Spec.SecurityContext.HostIPC, "Host IPC is not allowed to be used"))
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, s.strategies.SysctlsStrategy.Validate(pod)...)
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
|
92
pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns.go
Normal file
92
pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns.go
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// mustMatchPatterns implements the CapabilitiesStrategy interface
|
||||
type mustMatchPatterns struct {
|
||||
patterns []string
|
||||
}
|
||||
|
||||
var (
|
||||
_ SysctlsStrategy = &mustMatchPatterns{}
|
||||
|
||||
defaultSysctlsPatterns = []string{"*"}
|
||||
)
|
||||
|
||||
// NewMustMatchPatterns creates a new mustMatchPattern strategy that will provide validation.
|
||||
// Passing nil means the default pattern, passing an empty list means to disallow all sysctls.
|
||||
func NewMustMatchPatterns(patterns []string) (SysctlsStrategy, error) {
|
||||
if patterns == nil {
|
||||
patterns = defaultSysctlsPatterns
|
||||
}
|
||||
return &mustMatchPatterns{
|
||||
patterns: patterns,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
func (s *mustMatchPatterns) Validate(pod *api.Pod) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
allErrs = append(allErrs, s.validateAnnotation(pod, api.SysctlsPodAnnotationKey)...)
|
||||
allErrs = append(allErrs, s.validateAnnotation(pod, api.UnsafeSysctlsPodAnnotationKey)...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func (s *mustMatchPatterns) validateAnnotation(pod *api.Pod, key string) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
fieldPath := field.NewPath("pod", "metadata", "annotations").Key(key)
|
||||
|
||||
sysctls, err := api.SysctlsFromPodAnnotation(pod.Annotations[key])
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fieldPath, pod.Annotations[key], err.Error()))
|
||||
}
|
||||
|
||||
if len(sysctls) > 0 {
|
||||
if len(s.patterns) == 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fieldPath, pod.Annotations[key], "sysctls are not allowed"))
|
||||
} else {
|
||||
for i, sysctl := range sysctls {
|
||||
allErrs = append(allErrs, s.ValidateSysctl(sysctl.Name, fieldPath.Index(i))...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func (s *mustMatchPatterns) ValidateSysctl(sysctlName string, fldPath *field.Path) field.ErrorList {
|
||||
for _, s := range s.patterns {
|
||||
if s[len(s)-1] == '*' {
|
||||
prefix := s[:len(s)-1]
|
||||
if strings.HasPrefix(sysctlName, string(prefix)) {
|
||||
return nil
|
||||
}
|
||||
} else if sysctlName == s {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return field.ErrorList{field.Forbidden(fldPath, fmt.Sprintf("sysctl %q is not allowed", sysctlName))}
|
||||
}
|
106
pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns_test.go
Normal file
106
pkg/security/podsecuritypolicy/sysctl/mustmatchpatterns_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
patterns []string
|
||||
allowed []string
|
||||
disallowed []string
|
||||
}{
|
||||
// no container requests
|
||||
"nil": {
|
||||
patterns: nil,
|
||||
allowed: []string{"foo"},
|
||||
},
|
||||
"empty": {
|
||||
patterns: []string{},
|
||||
disallowed: []string{"foo"},
|
||||
},
|
||||
"without wildcard": {
|
||||
patterns: []string{"a", "a.b"},
|
||||
allowed: []string{"a", "a.b"},
|
||||
disallowed: []string{"b"},
|
||||
},
|
||||
"with catch-all wildcard": {
|
||||
patterns: []string{"*"},
|
||||
allowed: []string{"a", "a.b"},
|
||||
},
|
||||
"with catch-all wildcard and non-wildcard": {
|
||||
patterns: []string{"a.b.c", "*"},
|
||||
allowed: []string{"a", "a.b", "a.b.c", "b"},
|
||||
},
|
||||
"without catch-all wildcard": {
|
||||
patterns: []string{"a.*", "b.*", "c.d.e", "d.e.f.*"},
|
||||
allowed: []string{"a.b", "b.c", "c.d.e", "d.e.f.g.h"},
|
||||
disallowed: []string{"a", "b", "c", "c.d", "d.e", "d.e.f"},
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
strategy, err := NewMustMatchPatterns(v.patterns)
|
||||
if err != nil {
|
||||
t.Errorf("%s failed: %v", k, err)
|
||||
continue
|
||||
}
|
||||
|
||||
pod := &api.Pod{}
|
||||
errs := strategy.Validate(pod)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("%s: unexpected validaton errors for empty sysctls: %v", k, errs)
|
||||
}
|
||||
|
||||
sysctls := []api.Sysctl{}
|
||||
for _, s := range v.allowed {
|
||||
sysctls = append(sysctls, api.Sysctl{
|
||||
Name: s,
|
||||
Value: "dummy",
|
||||
})
|
||||
}
|
||||
testAllowed := func(key string, category string) {
|
||||
pod.Annotations = map[string]string{
|
||||
key: api.PodAnnotationsFromSysctls(sysctls),
|
||||
}
|
||||
errs = strategy.Validate(pod)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("%s: unexpected validaton errors for %s sysctls: %v", k, category, errs)
|
||||
}
|
||||
}
|
||||
testDisallowed := func(key string, category string) {
|
||||
for _, s := range v.disallowed {
|
||||
pod.Annotations = map[string]string{
|
||||
key: api.PodAnnotationsFromSysctls([]api.Sysctl{{s, "dummy"}}),
|
||||
}
|
||||
errs = strategy.Validate(pod)
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("%s: expected error for %s sysctl %q", k, category, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testAllowed(api.SysctlsPodAnnotationKey, "safe")
|
||||
testAllowed(api.UnsafeSysctlsPodAnnotationKey, "unsafe")
|
||||
testDisallowed(api.SysctlsPodAnnotationKey, "safe")
|
||||
testDisallowed(api.UnsafeSysctlsPodAnnotationKey, "unsafe")
|
||||
}
|
||||
}
|
28
pkg/security/podsecuritypolicy/sysctl/types.go
Normal file
28
pkg/security/podsecuritypolicy/sysctl/types.go
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// SysctlsStrategy defines the interface for all sysctl strategies.
|
||||
type SysctlsStrategy interface {
|
||||
// Validate ensures that the specified values fall within the range of the strategy.
|
||||
Validate(pod *api.Pod) field.ErrorList
|
||||
}
|
@ -23,6 +23,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/capabilities"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/group"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/selinux"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/sysctl"
|
||||
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/user"
|
||||
"k8s.io/kubernetes/pkg/util/validation/field"
|
||||
)
|
||||
@ -63,4 +64,5 @@ type ProviderStrategies struct {
|
||||
FSGroupStrategy group.GroupStrategy
|
||||
SupplementalGroupStrategy group.GroupStrategy
|
||||
CapabilitiesStrategy capabilities.Strategy
|
||||
SysctlsStrategy sysctl.SysctlsStrategy
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import (
|
||||
|
||||
kadmission "k8s.io/kubernetes/pkg/admission"
|
||||
kapi "k8s.io/kubernetes/pkg/api"
|
||||
extensions "k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
"k8s.io/kubernetes/pkg/client/cache"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
@ -34,7 +34,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/security/apparmor"
|
||||
kpsp "k8s.io/kubernetes/pkg/security/podsecuritypolicy"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
diff "k8s.io/kubernetes/pkg/util/diff"
|
||||
"k8s.io/kubernetes/pkg/util/diff"
|
||||
)
|
||||
|
||||
const defaultContainerName = "test-c"
|
||||
@ -1028,6 +1028,151 @@ func TestAdmitReadOnlyRootFilesystem(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdmitSysctls(t *testing.T) {
|
||||
podWithSysctls := func(safeSysctls []string, unsafeSysctls []string) *kapi.Pod {
|
||||
pod := goodPod()
|
||||
dummySysctls := func(names []string) []kapi.Sysctl {
|
||||
sysctls := make([]kapi.Sysctl, len(names))
|
||||
for i, n := range names {
|
||||
sysctls[i].Name = n
|
||||
sysctls[i].Value = "dummy"
|
||||
}
|
||||
return sysctls
|
||||
}
|
||||
pod.Annotations[kapi.SysctlsPodAnnotationKey] = kapi.PodAnnotationsFromSysctls(dummySysctls(safeSysctls))
|
||||
pod.Annotations[kapi.UnsafeSysctlsPodAnnotationKey] = kapi.PodAnnotationsFromSysctls(dummySysctls(unsafeSysctls))
|
||||
return pod
|
||||
}
|
||||
|
||||
noSysctls := restrictivePSP()
|
||||
noSysctls.Name = "no sysctls"
|
||||
|
||||
emptySysctls := restrictivePSP()
|
||||
emptySysctls.Name = "empty sysctls"
|
||||
emptySysctls.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = ""
|
||||
|
||||
mixedSysctls := restrictivePSP()
|
||||
mixedSysctls.Name = "wildcard sysctls"
|
||||
mixedSysctls.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "a.*,b.*,c,d.e.f"
|
||||
|
||||
aSysctl := restrictivePSP()
|
||||
aSysctl.Name = "a sysctl"
|
||||
aSysctl.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "a"
|
||||
|
||||
bSysctl := restrictivePSP()
|
||||
bSysctl.Name = "b sysctl"
|
||||
bSysctl.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "b"
|
||||
|
||||
cSysctl := restrictivePSP()
|
||||
cSysctl.Name = "c sysctl"
|
||||
cSysctl.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "c"
|
||||
|
||||
catchallSysctls := restrictivePSP()
|
||||
catchallSysctls.Name = "catchall sysctl"
|
||||
catchallSysctls.Annotations[extensions.SysctlsPodSecurityPolicyAnnotationKey] = "*"
|
||||
|
||||
tests := map[string]struct {
|
||||
pod *kapi.Pod
|
||||
psps []*extensions.PodSecurityPolicy
|
||||
shouldPass bool
|
||||
expectedPSP string
|
||||
}{
|
||||
"pod without unsafe sysctls request allowed under noSysctls PSP": {
|
||||
pod: goodPod(),
|
||||
psps: []*extensions.PodSecurityPolicy{noSysctls},
|
||||
shouldPass: true,
|
||||
expectedPSP: noSysctls.Name,
|
||||
},
|
||||
"pod without any sysctls request allowed under emptySysctls PSP": {
|
||||
pod: goodPod(),
|
||||
psps: []*extensions.PodSecurityPolicy{emptySysctls},
|
||||
shouldPass: true,
|
||||
expectedPSP: emptySysctls.Name,
|
||||
},
|
||||
"pod with safe sysctls request allowed under noSysctls PSP": {
|
||||
pod: podWithSysctls([]string{"a", "b"}, []string{}),
|
||||
psps: []*extensions.PodSecurityPolicy{noSysctls},
|
||||
shouldPass: true,
|
||||
expectedPSP: noSysctls.Name,
|
||||
},
|
||||
"pod with unsafe sysctls request allowed under noSysctls PSP": {
|
||||
pod: podWithSysctls([]string{}, []string{"a", "b"}),
|
||||
psps: []*extensions.PodSecurityPolicy{noSysctls},
|
||||
shouldPass: true,
|
||||
expectedPSP: noSysctls.Name,
|
||||
},
|
||||
"pod with safe sysctls request disallowed under emptySysctls PSP": {
|
||||
pod: podWithSysctls([]string{"a", "b"}, []string{}),
|
||||
psps: []*extensions.PodSecurityPolicy{emptySysctls},
|
||||
shouldPass: false,
|
||||
},
|
||||
"pod with unsafe sysctls request disallowed under emptySysctls PSP": {
|
||||
pod: podWithSysctls([]string{}, []string{"a", "b"}),
|
||||
psps: []*extensions.PodSecurityPolicy{emptySysctls},
|
||||
shouldPass: false,
|
||||
},
|
||||
"pod with matching sysctls request allowed under mixedSysctls PSP": {
|
||||
pod: podWithSysctls([]string{"a.b", "b.c"}, []string{"c", "d.e.f"}),
|
||||
psps: []*extensions.PodSecurityPolicy{mixedSysctls},
|
||||
shouldPass: true,
|
||||
expectedPSP: mixedSysctls.Name,
|
||||
},
|
||||
"pod with not-matching unsafe sysctls request allowed under mixedSysctls PSP": {
|
||||
pod: podWithSysctls([]string{"a.b", "b.c", "c", "d.e.f"}, []string{"e"}),
|
||||
psps: []*extensions.PodSecurityPolicy{mixedSysctls},
|
||||
shouldPass: false,
|
||||
},
|
||||
"pod with not-matching safe sysctls request allowed under mixedSysctls PSP": {
|
||||
pod: podWithSysctls([]string{"a.b", "b.c", "c", "d.e.f", "e"}, []string{}),
|
||||
psps: []*extensions.PodSecurityPolicy{mixedSysctls},
|
||||
shouldPass: false,
|
||||
},
|
||||
"pod with sysctls request allowed under catchallSysctls PSP": {
|
||||
pod: podWithSysctls([]string{"e"}, []string{"f"}),
|
||||
psps: []*extensions.PodSecurityPolicy{catchallSysctls},
|
||||
shouldPass: true,
|
||||
expectedPSP: catchallSysctls.Name,
|
||||
},
|
||||
"pod with sysctls request allowed under catchallSysctls PSP, not under mixedSysctls or emptySysctls PSP": {
|
||||
pod: podWithSysctls([]string{"e"}, []string{"f"}),
|
||||
psps: []*extensions.PodSecurityPolicy{mixedSysctls, catchallSysctls, emptySysctls},
|
||||
shouldPass: true,
|
||||
expectedPSP: catchallSysctls.Name,
|
||||
},
|
||||
"pod with safe c sysctl request allowed under cSysctl PSP, not under aSysctl or bSysctl PSP": {
|
||||
pod: podWithSysctls([]string{}, []string{"c"}),
|
||||
psps: []*extensions.PodSecurityPolicy{aSysctl, bSysctl, cSysctl},
|
||||
shouldPass: true,
|
||||
expectedPSP: cSysctl.Name,
|
||||
},
|
||||
"pod with unsafe c sysctl request allowed under cSysctl PSP, not under aSysctl or bSysctl PSP": {
|
||||
pod: podWithSysctls([]string{"c"}, []string{}),
|
||||
psps: []*extensions.PodSecurityPolicy{aSysctl, bSysctl, cSysctl},
|
||||
shouldPass: true,
|
||||
expectedPSP: cSysctl.Name,
|
||||
},
|
||||
}
|
||||
|
||||
for k, v := range tests {
|
||||
origSafeSysctls, origUnsafeSysctls, err := kapi.SysctlsFromPodAnnotations(v.pod.Annotations)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid sysctl annotation: %v", err)
|
||||
}
|
||||
|
||||
testPSPAdmit(k, v.psps, v.pod, v.shouldPass, v.expectedPSP, t)
|
||||
|
||||
if v.shouldPass {
|
||||
safeSysctls, unsafeSysctls, _ := kapi.SysctlsFromPodAnnotations(v.pod.Annotations)
|
||||
if !reflect.DeepEqual(safeSysctls, origSafeSysctls) {
|
||||
t.Errorf("%s: wrong safe sysctls: expected=%v, got=%v", k, origSafeSysctls, safeSysctls)
|
||||
}
|
||||
if !reflect.DeepEqual(unsafeSysctls, origUnsafeSysctls) {
|
||||
t.Errorf("%s: wrong unsafe sysctls: expected=%v, got=%v", k, origSafeSysctls, safeSysctls)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testPSPAdmit(testCaseName string, psps []*extensions.PodSecurityPolicy, pod *kapi.Pod, shouldPass bool, expectedPSP string, t *testing.T) {
|
||||
namespace := createNamespaceForTest()
|
||||
serviceAccount := createSAForTest()
|
||||
@ -1044,17 +1189,17 @@ func testPSPAdmit(testCaseName string, psps []*extensions.PodSecurityPolicy, pod
|
||||
err := plugin.Admit(attrs)
|
||||
|
||||
if shouldPass && err != nil {
|
||||
t.Errorf("%s expected no errors but received %v", testCaseName, err)
|
||||
t.Errorf("%s: expected no errors but received %v", testCaseName, err)
|
||||
}
|
||||
|
||||
if shouldPass && err == nil {
|
||||
if pod.Annotations[psputil.ValidatedPSPAnnotation] != expectedPSP {
|
||||
t.Errorf("%s expected to validate under %s but found %s", testCaseName, expectedPSP, pod.Annotations[psputil.ValidatedPSPAnnotation])
|
||||
t.Errorf("%s: expected to validate under %s but found %s", testCaseName, expectedPSP, pod.Annotations[psputil.ValidatedPSPAnnotation])
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldPass && err == nil {
|
||||
t.Errorf("%s expected errors but received none", testCaseName)
|
||||
t.Errorf("%s: expected errors but received none", testCaseName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1238,7 +1383,8 @@ func TestCreateProvidersFromConstraints(t *testing.T) {
|
||||
func restrictivePSP() *extensions.PodSecurityPolicy {
|
||||
return &extensions.PodSecurityPolicy{
|
||||
ObjectMeta: kapi.ObjectMeta{
|
||||
Name: "restrictive",
|
||||
Name: "restrictive",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: extensions.PodSecurityPolicySpec{
|
||||
RunAsUser: extensions.RunAsUserStrategyOptions{
|
||||
@ -1291,6 +1437,9 @@ func createSAForTest() *kapi.ServiceAccount {
|
||||
// psp when defaults are filled in.
|
||||
func goodPod() *kapi.Pod {
|
||||
return &kapi.Pod{
|
||||
ObjectMeta: kapi.ObjectMeta{
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: kapi.PodSpec{
|
||||
ServiceAccountName: "default",
|
||||
SecurityContext: &kapi.PodSecurityContext{},
|
||||
|
234
test/e2e/common/sysctl.go
Normal file
234
test/e2e/common/sysctl.go
Normal file
@ -0,0 +1,234 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/kubelet/events"
|
||||
"k8s.io/kubernetes/pkg/kubelet/sysctl"
|
||||
"k8s.io/kubernetes/pkg/util/uuid"
|
||||
"k8s.io/kubernetes/pkg/util/wait"
|
||||
"k8s.io/kubernetes/test/e2e/framework"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = framework.KubeDescribe("Sysctls", func() {
|
||||
f := framework.NewDefaultFramework("sysctl")
|
||||
var podClient *framework.PodClient
|
||||
|
||||
testPod := func() *api.Pod {
|
||||
podName := "sysctl-" + string(uuid.NewUUID())
|
||||
pod := api.Pod{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: podName,
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: "test-container",
|
||||
Image: "gcr.io/google_containers/busybox:1.24",
|
||||
},
|
||||
},
|
||||
RestartPolicy: api.RestartPolicyNever,
|
||||
},
|
||||
}
|
||||
|
||||
return &pod
|
||||
}
|
||||
|
||||
waitForPodErrorEventOrStarted := func(pod *api.Pod) (*api.Event, error) {
|
||||
var ev *api.Event
|
||||
err := wait.Poll(framework.Poll, framework.PodStartTimeout, func() (bool, error) {
|
||||
evnts, err := f.Client.Events(pod.Namespace).Search(pod)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error in listing events: %s", err)
|
||||
}
|
||||
for _, e := range evnts.Items {
|
||||
switch e.Reason {
|
||||
case sysctl.UnsupportedReason, sysctl.ForbiddenReason:
|
||||
ev = &e
|
||||
return true, nil
|
||||
case events.StartedContainer:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
return ev, err
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
podClient = f.PodClient()
|
||||
})
|
||||
|
||||
It("should support sysctls", func() {
|
||||
pod := testPod()
|
||||
pod.Annotations[api.SysctlsPodAnnotationKey] = api.PodAnnotationsFromSysctls([]api.Sysctl{
|
||||
{
|
||||
Name: "kernel.shm_rmid_forced",
|
||||
Value: "1",
|
||||
},
|
||||
})
|
||||
pod.Spec.Containers[0].Command = []string{"/bin/sysctl", "kernel.shm_rmid_forced"}
|
||||
|
||||
By("Creating a pod with the kernel.shm_rmid_forced sysctl")
|
||||
pod = podClient.Create(pod)
|
||||
|
||||
By("Watching for error events or started pod")
|
||||
// watch for events instead of termination of pod because the kubelet deletes
|
||||
// failed pods without running containers. This would create a race as the pod
|
||||
// might have already been deleted here.
|
||||
ev, err := waitForPodErrorEventOrStarted(pod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
if ev != nil && ev.Reason == sysctl.UnsupportedReason {
|
||||
framework.Skipf("No sysctl support in Docker <1.12")
|
||||
}
|
||||
Expect(ev).To(BeNil())
|
||||
|
||||
By("Waiting for pod completion")
|
||||
err = f.WaitForPodNoLongerRunning(pod.Name)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
pod, err = podClient.Get(pod.Name)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Checking that the pod succeeded")
|
||||
Expect(pod.Status.Phase).To(Equal(api.PodSucceeded))
|
||||
|
||||
By("Getting logs from the pod")
|
||||
log, err := framework.GetPodLogs(f.Client, f.Namespace.Name, pod.Name, pod.Spec.Containers[0].Name)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Checking that the sysctl is actually updated")
|
||||
Expect(log).To(ContainSubstring("kernel.shm_rmid_forced = 1"))
|
||||
})
|
||||
|
||||
It("should support unsafe sysctls which are actually whitelisted", func() {
|
||||
pod := testPod()
|
||||
pod.Annotations[api.UnsafeSysctlsPodAnnotationKey] = api.PodAnnotationsFromSysctls([]api.Sysctl{
|
||||
{
|
||||
Name: "kernel.shm_rmid_forced",
|
||||
Value: "1",
|
||||
},
|
||||
})
|
||||
pod.Spec.Containers[0].Command = []string{"/bin/sysctl", "kernel.shm_rmid_forced"}
|
||||
|
||||
By("Creating a pod with the kernel.shm_rmid_forced sysctl")
|
||||
pod = podClient.Create(pod)
|
||||
|
||||
By("Watching for error events or started pod")
|
||||
// watch for events instead of termination of pod because the kubelet deletes
|
||||
// failed pods without running containers. This would create a race as the pod
|
||||
// might have already been deleted here.
|
||||
ev, err := waitForPodErrorEventOrStarted(pod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
if ev != nil && ev.Reason == sysctl.UnsupportedReason {
|
||||
framework.Skipf("No sysctl support in Docker <1.12")
|
||||
}
|
||||
Expect(ev).To(BeNil())
|
||||
|
||||
By("Waiting for pod completion")
|
||||
err = f.WaitForPodNoLongerRunning(pod.Name)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
pod, err = podClient.Get(pod.Name)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Checking that the pod succeeded")
|
||||
Expect(pod.Status.Phase).To(Equal(api.PodSucceeded))
|
||||
|
||||
By("Getting logs from the pod")
|
||||
log, err := framework.GetPodLogs(f.Client, f.Namespace.Name, pod.Name, pod.Spec.Containers[0].Name)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Checking that the sysctl is actually updated")
|
||||
Expect(log).To(ContainSubstring("kernel.shm_rmid_forced = 1"))
|
||||
})
|
||||
|
||||
It("should reject invalid sysctls", func() {
|
||||
pod := testPod()
|
||||
pod.Annotations[api.SysctlsPodAnnotationKey] = api.PodAnnotationsFromSysctls([]api.Sysctl{
|
||||
{
|
||||
Name: "foo-",
|
||||
Value: "bar",
|
||||
},
|
||||
{
|
||||
Name: "kernel.shmmax",
|
||||
Value: "100000000",
|
||||
},
|
||||
{
|
||||
Name: "safe-and-unsafe",
|
||||
Value: "100000000",
|
||||
},
|
||||
})
|
||||
pod.Annotations[api.UnsafeSysctlsPodAnnotationKey] = api.PodAnnotationsFromSysctls([]api.Sysctl{
|
||||
{
|
||||
Name: "kernel.shmall",
|
||||
Value: "100000000",
|
||||
},
|
||||
{
|
||||
Name: "bar..",
|
||||
Value: "42",
|
||||
},
|
||||
{
|
||||
Name: "safe-and-unsafe",
|
||||
Value: "100000000",
|
||||
},
|
||||
})
|
||||
|
||||
By("Creating a pod with one valid and two invalid sysctls")
|
||||
client := f.Client.Pods(f.Namespace.Name)
|
||||
_, err := client.Create(pod)
|
||||
defer client.Delete(pod.Name, nil)
|
||||
|
||||
Expect(err).NotTo(BeNil())
|
||||
Expect(err.Error()).To(ContainSubstring(`Invalid value: "foo-"`))
|
||||
Expect(err.Error()).To(ContainSubstring(`Invalid value: "bar.."`))
|
||||
Expect(err.Error()).To(ContainSubstring(`safe-and-unsafe`))
|
||||
Expect(err.Error()).NotTo(ContainSubstring("kernel.shmmax"))
|
||||
})
|
||||
|
||||
It("should not launch unsafe, but not explicitly enabled sysctls on the node", func() {
|
||||
pod := testPod()
|
||||
pod.Annotations[api.SysctlsPodAnnotationKey] = api.PodAnnotationsFromSysctls([]api.Sysctl{
|
||||
{
|
||||
Name: "kernel.msgmax",
|
||||
Value: "10000000000",
|
||||
},
|
||||
})
|
||||
|
||||
By("Creating a pod with a greylisted, but not whitelisted sysctl on the node")
|
||||
pod = podClient.Create(pod)
|
||||
|
||||
By("Watching for error events or started pod")
|
||||
// watch for events instead of termination of pod because the kubelet deletes
|
||||
// failed pods without running containers. This would create a race as the pod
|
||||
// might have already been deleted here.
|
||||
ev, err := waitForPodErrorEventOrStarted(pod)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
if ev != nil && ev.Reason == sysctl.UnsupportedReason {
|
||||
framework.Skipf("No sysctl support in Docker <1.12")
|
||||
}
|
||||
|
||||
By("Checking that the pod was rejected")
|
||||
Expect(ev).ToNot(BeNil())
|
||||
Expect(ev.Reason).To(Equal("SysctlForbidden"))
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user