diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index aa3390f97..0606a602a 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -89,6 +89,7 @@ "allowDeviceMappingForRegularUsers": true, "allowHostNamespaceForRegularUsers": true, "allowPrivilegedModeForRegularUsers": true, + "allowSecurityOptForRegularUsers": false, "allowStackManagementForRegularUsers": true, "allowSysctlSettingForRegularUsers": false, "allowVolumeBrowserForRegularUsers": false, diff --git a/api/http/handler/endpoints/endpoint_settings_update.go b/api/http/handler/endpoints/endpoint_settings_update.go index 91b668b0f..e960b1f05 100644 --- a/api/http/handler/endpoints/endpoint_settings_update.go +++ b/api/http/handler/endpoints/endpoint_settings_update.go @@ -26,6 +26,8 @@ type endpointSettingsUpdatePayload struct { AllowContainerCapabilitiesForRegularUsers *bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"` // Whether non-administrator should be able to use sysctl settings AllowSysctlSettingForRegularUsers *bool `json:"allowSysctlSettingForRegularUsers" example:"true"` + // Whether non-administrator should be able to use security-opt settings + AllowSecurityOptForRegularUsers *bool `json:"allowSecurityOptForRegularUsers" example:"true"` // Whether host management features are enabled EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures" example:"true"` @@ -111,6 +113,12 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures } + if payload.AllowSecurityOptForRegularUsers != nil { + securitySettings.AllowSecurityOptForRegularUsers = *payload.AllowSecurityOptForRegularUsers + } + + endpoint.SecuritySettings = securitySettings + if payload.EnableGPUManagement != nil { endpoint.EnableGPUManagement = *payload.EnableGPUManagement } @@ -119,8 +127,6 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re endpoint.Gpus = payload.Gpus } - endpoint.SecuritySettings = securitySettings - err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint) if err != nil { return httperror.InternalServerError("Failed persisting environment in database", err) diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index 765f5f47b..0c7788357 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -25,6 +25,7 @@ var ( ErrPIDHostNamespaceForbidden = errors.New("forbidden to use pid host namespace") ErrDeviceMappingForbidden = errors.New("forbidden to use device mapping") ErrSysCtlSettingsForbidden = errors.New("forbidden to use sysctl settings") + ErrSecurityOptSettingsForbidden = errors.New("forbidden to use security-opt settings") ErrContainerCapabilitiesForbidden = errors.New("forbidden to use container capabilities") ErrBindMountsForbidden = errors.New("forbidden to use bind mounts") ) @@ -90,7 +91,7 @@ func (transport *Transport) containerListOperation(response *http.Response, exec // containerInspectOperation extracts the response as a JSON object, verify that the user // has access to the container based on resource control and either rewrite an access denied response or a decorated container. func (transport *Transport) containerInspectOperation(response *http.Response, executor *operationExecutor) error { - //ContainerInspect response is a JSON object + // ContainerInspect response is a JSON object // https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect responseObject, err := utils.GetResponseAsJSONObject(response) if err != nil { @@ -116,6 +117,7 @@ func selectorContainerLabelsFromContainerInspectOperation(responseObject map[str containerConfigObject := utils.GetJSONObject(responseObject, "Config") if containerConfigObject != nil { containerLabelsObject := utils.GetJSONObject(containerConfigObject, "Labels") + return containerLabelsObject } @@ -170,13 +172,14 @@ func containerHasBlackListedLabel(containerLabels map[string]any, labelBlackList 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 []any `json:"Devices"` - Sysctls map[string]any `json:"Sysctls"` - CapAdd []string `json:"CapAdd"` - CapDrop []string `json:"CapDrop"` - Binds []string `json:"Binds"` + Privileged bool `json:"Privileged"` + PidMode string `json:"PidMode"` + Devices []any `json:"Devices"` + Sysctls map[string]any `json:"Sysctls"` + SecurityOpt []string `json:"SecurityOpt"` + CapAdd []string `json:"CapAdd"` + CapDrop []string `json:"CapDrop"` + Binds []string `json:"Binds"` } `json:"HostConfig"` } @@ -226,6 +229,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return forbiddenResponse, ErrSysCtlSettingsForbidden } + if !securitySettings.AllowSecurityOptForRegularUsers && len(partialContainer.HostConfig.SecurityOpt) > 0 { + return forbiddenResponse, ErrSecurityOptSettingsForbidden + } + if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { return nil, ErrContainerCapabilitiesForbidden } diff --git a/api/portainer.go b/api/portainer.go index 7b48dc2e6..8a2215781 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -645,6 +645,8 @@ type ( AllowContainerCapabilitiesForRegularUsers bool `json:"allowContainerCapabilitiesForRegularUsers" example:"true"` // Whether non-administrator should be able to use sysctl settings AllowSysctlSettingForRegularUsers bool `json:"allowSysctlSettingForRegularUsers" example:"true"` + // Whether non-administrator should be able to use security-opt settings + AllowSecurityOptForRegularUsers bool `json:"allowSecurityOptForRegularUsers" example:"true"` // Whether host management features are enabled EnableHostManagementFeatures bool `json:"enableHostManagementFeatures" example:"true"` } @@ -2477,6 +2479,7 @@ func DefaultEndpointSecuritySettings() EndpointSecuritySettings { AllowHostNamespaceForRegularUsers: false, AllowPrivilegedModeForRegularUsers: false, AllowSysctlSettingForRegularUsers: false, + AllowSecurityOptForRegularUsers: false, AllowVolumeBrowserForRegularUsers: false, EnableHostManagementFeatures: false, diff --git a/api/stacks/stackutils/validation.go b/api/stacks/stackutils/validation.go index 1c4c71162..d9f58a533 100644 --- a/api/stacks/stackutils/validation.go +++ b/api/stacks/stackutils/validation.go @@ -56,6 +56,10 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo return errors.New("sysctl setting disabled for non administrator users") } + if !securitySettings.AllowSecurityOptForRegularUsers && len(service.SecurityOpt) > 0 { + return errors.New("security-opt 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/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js b/app/docker/views/docker-features-configuration/docker-features-configuration.controller.js index 9db6919d1..e339626d7 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 @@ -24,6 +24,7 @@ export default class DockerFeaturesConfigurationController { disableDeviceMappingForRegularUsers: false, disableContainerCapabilitiesForRegularUsers: false, disableSysctlSettingForRegularUsers: false, + disableSecurityOptForRegularUsers: false, }; this.isAgent = false; @@ -48,6 +49,7 @@ export default class DockerFeaturesConfigurationController { this.onChangeDisableDeviceMappingForRegularUsers = this.onChangeField('disableDeviceMappingForRegularUsers'); this.onChangeDisableContainerCapabilitiesForRegularUsers = this.onChangeField('disableContainerCapabilitiesForRegularUsers'); this.onChangeDisableSysctlSettingForRegularUsers = this.onChangeField('disableSysctlSettingForRegularUsers'); + this.onChangeDisableSecurityOptForRegularUsers = this.onChangeField('disableSecurityOptForRegularUsers'); } onToggleAutoUpdate(value) { @@ -93,6 +95,7 @@ export default class DockerFeaturesConfigurationController { disableDeviceMappingForRegularUsers, disableContainerCapabilitiesForRegularUsers, disableSysctlSettingForRegularUsers, + disableSecurityOptForRegularUsers, } = this.formValues; return ( disableBindMountsForRegularUsers || @@ -100,7 +103,8 @@ export default class DockerFeaturesConfigurationController { disablePrivilegedModeForRegularUsers || disableDeviceMappingForRegularUsers || disableContainerCapabilitiesForRegularUsers || - disableSysctlSettingForRegularUsers + disableSysctlSettingForRegularUsers || + disableSecurityOptForRegularUsers ); } @@ -122,6 +126,7 @@ export default class DockerFeaturesConfigurationController { allowStackManagementForRegularUsers: !this.formValues.disableStackManagementForRegularUsers, allowContainerCapabilitiesForRegularUsers: !this.formValues.disableContainerCapabilitiesForRegularUsers, allowSysctlSettingForRegularUsers: !this.formValues.disableSysctlSettingForRegularUsers, + allowSecurityOptForRegularUsers: !this.formValues.disableSecurityOptForRegularUsers, enableGPUManagement: this.state.enableGPUManagement, gpus, }; @@ -159,6 +164,7 @@ export default class DockerFeaturesConfigurationController { disableStackManagementForRegularUsers: !securitySettings.allowStackManagementForRegularUsers, disableContainerCapabilitiesForRegularUsers: !securitySettings.allowContainerCapabilitiesForRegularUsers, disableSysctlSettingForRegularUsers: !securitySettings.allowSysctlSettingForRegularUsers, + disableSecurityOptForRegularUsers: !securitySettings.allowSecurityOptForRegularUsers, }; // this.endpoint.Gpus could be null as it is Gpus: []Pair in the API 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 8edd5843f..ac325b4bf 100644 --- a/app/docker/views/docker-features-configuration/docker-features-configuration.html +++ b/app/docker/views/docker-features-configuration/docker-features-configuration.html @@ -142,6 +142,17 @@ > +
| {value} | +