diff --git a/api/http/handler/endpoints/endpoint_settings_update.go b/api/http/handler/endpoints/endpoint_settings_update.go index 8684e4e7f..1404c0470 100644 --- a/api/http/handler/endpoints/endpoint_settings_update.go +++ b/api/http/handler/endpoints/endpoint_settings_update.go @@ -12,14 +12,24 @@ import ( ) type endpointSettingsUpdatePayload struct { - AllowBindMountsForRegularUsers *bool `json:"allowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers *bool `json:"allowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers *bool `json:"allowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers *bool `json:"allowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers *bool `json:"allowDeviceMappingForRegularUsers"` - AllowStackManagementForRegularUsers *bool `json:"allowStackManagementForRegularUsers"` - AllowContainerCapabilitiesForRegularUsers *bool `json:"allowContainerCapabilitiesForRegularUsers"` - EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures"` + // Whether non-administrator should be able to use bind mounts when creating containers + AllowBindMountsForRegularUsers *bool `json:"allowBindMountsForRegularUsers" example:"false"` + // Whether non-administrator should be able to use privileged mode when creating containers + AllowPrivilegedModeForRegularUsers *bool `json:"allowPrivilegedModeForRegularUsers" example:"false"` + // Whether non-administrator should be able to browse volumes + AllowVolumeBrowserForRegularUsers *bool `json:"allowVolumeBrowserForRegularUsers" example:"true"` + // Whether non-administrator should be able to use the host pid + AllowHostNamespaceForRegularUsers *bool `json:"allowHostNamespaceForRegularUsers" example:"true"` + // Whether non-administrator should be able to use device mapping + AllowDeviceMappingForRegularUsers *bool `json:"allowDeviceMappingForRegularUsers" example:"true"` + // Whether non-administrator should be able to manage stacks + 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"` } func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error { @@ -83,6 +93,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 b0c08979f..1ace49de2 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -349,6 +349,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 3115914af..b979dd3cd 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -165,6 +165,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 9396cc20a..fe85f246c 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 5b7fcc5ad..637f23ca0 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -307,14 +307,24 @@ type ( // EndpointSecuritySettings represents settings for an endpoint EndpointSecuritySettings struct { - AllowBindMountsForRegularUsers bool `json:"allowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"allowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"allowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"allowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"allowDeviceMappingForRegularUsers"` - AllowStackManagementForRegularUsers bool `json:"allowStackManagementForRegularUsers"` - AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers"` - EnableHostManagementFeatures bool `json:"enableHostManagementFeatures"` + // Whether non-administrator should be able to use bind mounts when creating containers + AllowBindMountsForRegularUsers bool `json:"allowBindMountsForRegularUsers" example:"false"` + // Whether non-administrator should be able to use privileged mode when creating containers + AllowPrivilegedModeForRegularUsers bool `json:"allowPrivilegedModeForRegularUsers" example:"false"` + // Whether non-administrator should be able to browse volumes + AllowVolumeBrowserForRegularUsers bool `json:"allowVolumeBrowserForRegularUsers" example:"true"` + // Whether non-administrator should be able to use the host pid + AllowHostNamespaceForRegularUsers bool `json:"allowHostNamespaceForRegularUsers" example:"true"` + // Whether non-administrator should be able to use device mapping + AllowDeviceMappingForRegularUsers bool `json:"allowDeviceMappingForRegularUsers" example:"true"` + // Whether non-administrator should be able to manage stacks + 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"` } // EndpointType represents the type of an endpoint diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 070f6e11c..4e9b47aa0 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -79,6 +79,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ EntrypointMode: 'default', NodeName: null, capabilities: [], + Sysctls: [], LogDriverName: '', LogDriverOpts: [], RegistryModel: new PorImageRegistryModel(), @@ -126,6 +127,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ Devices: [], CapAdd: [], CapDrop: [], + Sysctls: {}, }, NetworkingConfig: { EndpointsConfig: {}, @@ -181,6 +183,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: '' }); }; @@ -334,6 +344,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) { @@ -402,6 +422,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ prepareResources(config); prepareLogDriver(config); prepareCapabilities(config); + prepareSysctls(config); return config; } @@ -547,6 +568,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) => { @@ -622,6 +651,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'); @@ -710,6 +740,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.allowBindMounts = checkIfAdminOrEndpointAdmin() || endpoint.SecuritySettings.allowBindMountsForRegularUsers; $scope.allowPrivilegedMode = checkIfAdminOrEndpointAdmin() || endpoint.SecuritySettings.allowPrivilegedModeForRegularUsers; + $scope.allowSysctl = checkIfAdminOrEndpointAdmin() || endpoint.SecuritySettings.AllowSysctlSettingForRegularUsers; PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) { $scope.availableLoggingDrivers = loggingDrivers; diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 895f8112d..f82aed772 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -699,6 +699,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 95affc028..37501a532 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -103,6 +103,7 @@ angular.module('portainer.docker').controller('ContainerController', [ allowContainerCapabilitiesForRegularUsers, allowHostNamespaceForRegularUsers, allowDeviceMappingForRegularUsers, + allowSysctlSettingForRegularUsers, allowBindMountsForRegularUsers, allowPrivilegedModeForRegularUsers, } = endpoint.SecuritySettings; @@ -111,6 +112,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/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' }];