From d09ae22ba8001c09ffd897c7363b7afbf3c360d7 Mon Sep 17 00:00:00 2001 From: Lukas Grotz Date: Mon, 12 Apr 2021 09:40:45 +0200 Subject: [PATCH] feat(container): add sysctls setting in the container view (#4910) * feat(container): add sysctls in the container view (#2756) * feat(container): add setting to restrict sysctl access * feat(endpoint): move sysctl disable setting to security settings * feat(container): add sysctls to container edit view * fix(container) remove unnecessary migration setting Co-authored-by: Owen Kirby --- .../endpoints/endpoint_settings_update.go | 6 +++ .../handler/stacks/create_compose_stack.go | 1 + api/http/handler/stacks/stack_create.go | 4 ++ api/http/proxy/factory/docker/containers.go | 17 ++++++--- api/portainer.go | 2 + .../create/createContainerController.js | 37 +++++++++++++++++++ .../containers/create/createcontainer.html | 27 ++++++++++++++ .../views/containers/edit/container.html | 11 ++++++ .../containers/edit/containerController.js | 2 + ...ocker-features-configuration.controller.js | 7 +++- .../docker-features-configuration.html | 10 +++++ .../kubernetesConfigurationData.html | 6 ++- app/kubernetes/pod/converter.js | 2 +- .../startContainerController.spec.js | 2 + 14 files changed, 125 insertions(+), 9 deletions(-) diff --git a/api/http/handler/endpoints/endpoint_settings_update.go b/api/http/handler/endpoints/endpoint_settings_update.go index ea863a344..5cccf5aed 100644 --- a/api/http/handler/endpoints/endpoint_settings_update.go +++ b/api/http/handler/endpoints/endpoint_settings_update.go @@ -25,6 +25,8 @@ type endpointSettingsUpdatePayload struct { AllowStackManagementForRegularUsers *bool `json:"allowStackManagementForRegularUsers" example:"true"` // Whether non-administrator should be able to use container capabilities AllowContainerCapabilitiesForRegularUsers *bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"` + // Whether non-administrator should be able to use sysctl settings + AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"` // Whether host management features are enabled EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures" example:"true"` } @@ -97,6 +99,10 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re securitySettings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers } + if payload.AllowSysctlSettingForRegularUsers != nil { + securitySettings.AllowSysctlSettingForRegularUsers = *payload.AllowSysctlSettingForRegularUsers + } + if payload.EnableHostManagementFeatures != nil { securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures } diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 15658fee5..2c78ee0f9 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -356,6 +356,7 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) !securitySettings.AllowPrivilegedModeForRegularUsers || !securitySettings.AllowHostNamespaceForRegularUsers || !securitySettings.AllowDeviceMappingForRegularUsers || + !securitySettings.AllowSysctlSettingForRegularUsers || !securitySettings.AllowContainerCapabilitiesForRegularUsers) && !isAdminOrEndpointAdmin { diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index d3ffe2b53..7da0fd386 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -192,6 +192,10 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettin return errors.New("device mapping disabled for non administrator users") } + if !securitySettings.AllowSysctlSettingForRegularUsers && service.Sysctls != nil && len(service.Sysctls) > 0 { + return errors.New("sysctl setting disabled for non administrator users") + } + if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) { return errors.New("container capabilities disabled for non administrator users") } diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index 68a90d55b..97108355e 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -152,12 +152,13 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) { type PartialContainer struct { HostConfig struct { - Privileged bool `json:"Privileged"` - PidMode string `json:"PidMode"` - Devices []interface{} `json:"Devices"` - CapAdd []string `json:"CapAdd"` - CapDrop []string `json:"CapDrop"` - Binds []string `json:"Binds"` + Privileged bool `json:"Privileged"` + PidMode string `json:"PidMode"` + Devices []interface{} `json:"Devices"` + Sysctls map[string]interface{} `json:"Sysctls"` + CapAdd []string `json:"CapAdd"` + CapDrop []string `json:"CapDrop"` + Binds []string `json:"Binds"` } `json:"HostConfig"` } @@ -204,6 +205,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return forbiddenResponse, errors.New("forbidden to use device mapping") } + if !securitySettings.AllowSysctlSettingForRegularUsers && len(partialContainer.HostConfig.Sysctls) > 0 { + return forbiddenResponse, errors.New("forbidden to use sysctl settings") + } + if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { return nil, errors.New("forbidden to use container capabilities") } diff --git a/api/portainer.go b/api/portainer.go index 1647e0846..ea0711372 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -335,6 +335,8 @@ type ( AllowStackManagementForRegularUsers bool `json:"allowStackManagementForRegularUsers" example:"true"` // Whether non-administrator should be able to use container capabilities AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"` + // Whether non-administrator should be able to use sysctl settings + AllowSysctlSettingForRegularUsers bool `json:"AllowSysctlSettingForRegularUsers" example:"true"` // Whether host management features are enabled EnableHostManagementFeatures bool `json:"enableHostManagementFeatures" example:"true"` } diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index fabed0128..768d1e589 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -80,6 +80,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ EntrypointMode: 'default', NodeName: null, capabilities: [], + Sysctls: [], LogDriverName: '', LogDriverOpts: [], RegistryModel: new PorImageRegistryModel(), @@ -136,6 +137,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ Devices: [], CapAdd: [], CapDrop: [], + Sysctls: {}, }, NetworkingConfig: { EndpointsConfig: {}, @@ -191,6 +193,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.config.HostConfig.Devices.splice(index, 1); }; + $scope.addSysctl = function () { + $scope.formValues.Sysctls.push({ name: '', value: '' }); + }; + + $scope.removeSysctl = function (index) { + $scope.formValues.Sysctls.splice(index, 1); + }; + $scope.addLogDriverOpt = function () { $scope.formValues.LogDriverOpts.push({ name: '', value: '' }); }; @@ -344,6 +354,16 @@ angular.module('portainer.docker').controller('CreateContainerController', [ config.HostConfig.Devices = path; } + function prepareSysctls(config) { + var sysctls = {}; + $scope.formValues.Sysctls.forEach(function (sysctl) { + if (sysctl.name && sysctl.value) { + sysctls[sysctl.name] = sysctl.value; + } + }); + config.HostConfig.Sysctls = sysctls; + } + function prepareResources(config) { // Memory Limit - Round to 0.125 if ($scope.formValues.MemoryLimit >= 0) { @@ -412,6 +432,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ prepareResources(config); prepareLogDriver(config); prepareCapabilities(config); + prepareSysctls(config); return config; } @@ -557,6 +578,14 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.config.HostConfig.Devices = path; } + function loadFromContainerSysctls() { + for (var s in $scope.config.HostConfig.Sysctls) { + if ({}.hasOwnProperty.call($scope.config.HostConfig.Sysctls, s)) { + $scope.formValues.Sysctls.push({ name: s, value: $scope.config.HostConfig.Sysctls[s] }); + } + } + } + function loadFromContainerImageConfig() { RegistryService.retrievePorRegistryModelFromRepository($scope.config.Image) .then((model) => { @@ -632,6 +661,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ loadFromContainerImageConfig(d); loadFromContainerResources(d); loadFromContainerCapabilities(d); + loadFromContainerSysctls(d); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve container'); @@ -656,6 +686,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.isAdmin = Authentication.isAdmin(); $scope.showDeviceMapping = await shouldShowDevices(); + $scope.showSysctls = await shouldShowSysctls(); $scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled(); $scope.isAdminOrEndpointAdmin = Authentication.isAdmin(); @@ -940,6 +971,12 @@ angular.module('portainer.docker').controller('CreateContainerController', [ return endpoint.SecuritySettings.allowDeviceMappingForRegularUsers || Authentication.isAdmin(); } + async function shouldShowSysctls() { + const { allowSysctlSettingForRegularUsers } = $scope.applicationState.application; + + return allowSysctlSettingForRegularUsers || Authentication.isAdmin(); + } + async function checkIfContainerCapabilitiesEnabled() { return endpoint.SecuritySettings.allowContainerCapabilitiesForRegularUsers || Authentication.isAdmin(); } diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 9c5b63f87..de43d1fa2 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -706,6 +706,33 @@ + +
+
+ + + add sysctl + +
+ +
+
+
+ name + +
+
+ value + +
+ +
+
+ +
+
Resources
diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index 9506a865a..097162ce0 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -274,6 +274,17 @@ + + Sysctls + + + + + + +
{{ k }}{{ v }}
+ + diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index ffef2445e..83db4e944 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -104,6 +104,7 @@ angular.module('portainer.docker').controller('ContainerController', [ allowContainerCapabilitiesForRegularUsers, allowHostNamespaceForRegularUsers, allowDeviceMappingForRegularUsers, + allowSysctlSettingForRegularUsers, allowBindMountsForRegularUsers, allowPrivilegedModeForRegularUsers, } = endpoint.SecuritySettings; @@ -112,6 +113,7 @@ angular.module('portainer.docker').controller('ContainerController', [ !allowContainerCapabilitiesForRegularUsers || !allowBindMountsForRegularUsers || !allowDeviceMappingForRegularUsers || + !allowSysctlSettingForRegularUsers || !allowHostNamespaceForRegularUsers || !allowPrivilegedModeForRegularUsers; diff --git a/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js b/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js index bedec4867..e77d1b4c7 100644 --- a/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js @@ -15,6 +15,7 @@ export default class DockerFeaturesConfigurationController { disableStackManagementForRegularUsers: false, disableDeviceMappingForRegularUsers: false, disableContainerCapabilitiesForRegularUsers: false, + disableSysctlSettingForRegularUsers: false, }; this.isAgent = false; @@ -33,13 +34,15 @@ export default class DockerFeaturesConfigurationController { disablePrivilegedModeForRegularUsers, disableDeviceMappingForRegularUsers, disableContainerCapabilitiesForRegularUsers, + disableSysctlSettingForRegularUsers, } = this.formValues; return ( disableBindMountsForRegularUsers || disableHostNamespaceForRegularUsers || disablePrivilegedModeForRegularUsers || disableDeviceMappingForRegularUsers || - disableContainerCapabilitiesForRegularUsers + disableContainerCapabilitiesForRegularUsers || + disableSysctlSettingForRegularUsers ); } @@ -56,6 +59,7 @@ export default class DockerFeaturesConfigurationController { allowDeviceMappingForRegularUsers: !this.formValues.disableDeviceMappingForRegularUsers, allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers, allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers, + allowSysctlSettingForRegularUsers: !this.formValues.disableSysctlSettingForRegularUsers, }; await this.EndpointService.updateSecuritySettings(this.endpoint.Id, securitySettings); @@ -89,6 +93,7 @@ export default class DockerFeaturesConfigurationController { disableDeviceMappingForRegularUsers: !securitySettings.allowDeviceMappingForRegularUsers, disableStackManagementForRegularUsers: !securitySettings.allowStackManagementForRegularUsers, disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers, + disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers, }; } } diff --git a/app/docker/views/docker-features-configuration/docker-features-configuration.html b/app/docker/views/docker-features-configuration/docker-features-configuration.html index 0c5f033de..d906d86c6 100644 --- a/app/docker/views/docker-features-configuration/docker-features-configuration.html +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.html @@ -108,6 +108,16 @@ > +
+
+ +
+
diff --git a/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html index 22abaf506..ba6fca78d 100644 --- a/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html +++ b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html @@ -53,7 +53,11 @@

This field is required.

diff --git a/app/kubernetes/pod/converter.js b/app/kubernetes/pod/converter.js index 0317b0901..a39053209 100644 --- a/app/kubernetes/pod/converter.js +++ b/app/kubernetes/pod/converter.js @@ -9,8 +9,8 @@ import { KubernetesPortainerApplicationNote, } from 'Kubernetes/models/application/models'; -import { createPayloadFactory } from './payloads/create'; import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity, KubernetesPodContainer, KubernetesPodContainerTypes, KubernetesPodEviction } from 'Kubernetes/pod/models'; +import { createPayloadFactory } from './payloads/create'; function computeStatus(statuses) { const containerStatuses = _.map(statuses, 'state'); diff --git a/test/unit/app/components/startContainerController.spec.js b/test/unit/app/components/startContainerController.spec.js index 63885bb39..e2cc0c2b3 100644 --- a/test/unit/app/components/startContainerController.spec.js +++ b/test/unit/app/components/startContainerController.spec.js @@ -213,6 +213,7 @@ describe('startContainerController', function () { CgroupPermissions: 'mrw', }, ], + Sysctls: { 'net.ipv6.conf.all.disable_ipv6': '0' }, LxcConf: { 'lxc.utsname': 'docker' }, ExtraHosts: ['hostname:127.0.0.1'], RestartPolicy: { name: 'always', MaximumRetryCount: 5 }, @@ -255,6 +256,7 @@ describe('startContainerController', function () { CgroupPermissions: 'mrw', }, ]; + scope.config.HostConfig.Sysctls = [{ name: 'net.ipv6.conf.all.disable_ipv6', value: '0' }]; scope.config.HostConfig.LxcConf = [{ name: 'lxc.utsname', value: 'docker' }]; scope.config.HostConfig.ExtraHosts = [{ host: 'hostname', ip: '127.0.0.1' }];