mirror of https://github.com/portainer/portainer
feat(containers): add securityOpt
parent
f2e7680bf3
commit
8b47bdf8b4
|
@ -117,6 +117,7 @@ func setEndpointAuthorizations(endpoint *portainer.Endpoint) {
|
|||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowSecurityOptForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
|
|
|
@ -91,6 +91,7 @@
|
|||
"allowPrivilegedModeForRegularUsers": true,
|
||||
"allowStackManagementForRegularUsers": true,
|
||||
"allowSysctlSettingForRegularUsers": false,
|
||||
"allowSecurityOptForRegularUsers": false,
|
||||
"allowVolumeBrowserForRegularUsers": false,
|
||||
"enableHostManagementFeatures": false
|
||||
},
|
||||
|
|
|
@ -551,6 +551,7 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(tx dataservices.Data
|
|||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowSecurityOptForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
|
|
|
@ -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"`
|
||||
|
||||
|
@ -107,6 +109,10 @@ func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Re
|
|||
securitySettings.AllowSysctlSettingForRegularUsers = *payload.AllowSysctlSettingForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowSecurityOptForRegularUsers != nil {
|
||||
securitySettings.AllowSecurityOptForRegularUsers = *payload.AllowSecurityOptForRegularUsers
|
||||
}
|
||||
|
||||
if payload.EnableHostManagementFeatures != nil {
|
||||
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
||||
}
|
||||
|
|
|
@ -169,13 +169,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"`
|
||||
}
|
||||
|
||||
|
@ -225,6 +226,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
|||
return forbiddenResponse, ErrSysCtlSettingsForbidden
|
||||
}
|
||||
|
||||
if !securitySettings.AllowSecurityOptForRegularUsers && len(partialContainer.HostConfig.SecurityOpt) > 0 {
|
||||
return forbiddenResponse, errors.New("forbidden to use security-opt settings")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
|
||||
return nil, ErrContainerCapabilitiesForbidden
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.
|
|||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowSecurityOptForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
|
@ -161,6 +162,7 @@ func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStor
|
|||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowSecurityOptForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
|
|
|
@ -521,6 +521,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"`
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ func (config *ComposeStackDeploymentConfig) Deploy() error {
|
|||
!securitySettings.AllowHostNamespaceForRegularUsers ||
|
||||
!securitySettings.AllowDeviceMappingForRegularUsers ||
|
||||
!securitySettings.AllowSysctlSettingForRegularUsers ||
|
||||
!securitySettings.AllowSecurityOptForRegularUsers ||
|
||||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
|
||||
!isAdminOrEndpointAdmin {
|
||||
|
||||
|
|
|
@ -56,6 +56,10 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo
|
|||
return errors.New("sysctl setting disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowSecurityOptForRegularUsers && service.SecurityOpt != nil && 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")
|
||||
}
|
||||
|
|
|
@ -318,6 +318,16 @@
|
|||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!(container.HostConfig.SecurityOpt | emptyobject)">
|
||||
<td>SecurityOpt</td>
|
||||
<td>
|
||||
<table class="table-bordered table-condensed table">
|
||||
<tr ng-repeat="opt in container.HostConfig.SecurityOpt">
|
||||
<td>{{ opt }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="container.HostConfig.DeviceRequests.length">
|
||||
<td>GPUS</td>
|
||||
<td>{{ computeDockerGPUCommand() }}</td>
|
||||
|
|
|
@ -112,6 +112,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
|||
allowHostNamespaceForRegularUsers,
|
||||
allowDeviceMappingForRegularUsers,
|
||||
allowSysctlSettingForRegularUsers,
|
||||
allowSecurityOptForRegularUsers,
|
||||
allowBindMountsForRegularUsers,
|
||||
allowPrivilegedModeForRegularUsers,
|
||||
} = endpoint.SecuritySettings;
|
||||
|
@ -121,6 +122,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
|||
!allowBindMountsForRegularUsers ||
|
||||
!allowDeviceMappingForRegularUsers ||
|
||||
!allowSysctlSettingForRegularUsers ||
|
||||
!allowSecurityOptForRegularUsers ||
|
||||
!allowHostNamespaceForRegularUsers ||
|
||||
!allowPrivilegedModeForRegularUsers;
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ export default class DockerFeaturesConfigurationController {
|
|||
disableDeviceMappingForRegularUsers: false,
|
||||
disableContainerCapabilitiesForRegularUsers: false,
|
||||
disableSysctlSettingForRegularUsers: false,
|
||||
disableSecurityOptForRegularUsers: false,
|
||||
};
|
||||
|
||||
this.isAgent = false;
|
||||
|
@ -49,6 +50,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) {
|
||||
|
@ -94,6 +96,7 @@ export default class DockerFeaturesConfigurationController {
|
|||
disableDeviceMappingForRegularUsers,
|
||||
disableContainerCapabilitiesForRegularUsers,
|
||||
disableSysctlSettingForRegularUsers,
|
||||
disableSecurityOptForRegularUsers,
|
||||
} = this.formValues;
|
||||
return (
|
||||
disableBindMountsForRegularUsers ||
|
||||
|
@ -101,7 +104,8 @@ export default class DockerFeaturesConfigurationController {
|
|||
disablePrivilegedModeForRegularUsers ||
|
||||
disableDeviceMappingForRegularUsers ||
|
||||
disableContainerCapabilitiesForRegularUsers ||
|
||||
disableSysctlSettingForRegularUsers
|
||||
disableSysctlSettingForRegularUsers ||
|
||||
disableSecurityOptForRegularUsers
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -123,6 +127,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,
|
||||
};
|
||||
|
@ -177,6 +182,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
|
||||
|
|
|
@ -59,6 +59,20 @@
|
|||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="$ctrl.state.autoUpdateSettings.Enabled"
|
||||
name="'disableSecurityOptForRegularUsers'"
|
||||
label="'Enable Change Window'"
|
||||
label-class="'col-sm-7 col-lg-4'"
|
||||
feature-id="$ctrl.limitedFeatureAutoUpdate"
|
||||
tooltip="'Specify a time-frame during which GitOps updates can occur in this environment.'"
|
||||
on-change="($ctrl.onToggleAutoUpdate)"
|
||||
>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- security -->
|
||||
<div class="col-sm-12 form-section-title"> Docker security settings </div>
|
||||
|
@ -142,6 +156,17 @@
|
|||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="$ctrl.formValues.disableSecurityOptForRegularUsers"
|
||||
name="'disableSecurityOptForRegularUsers'"
|
||||
label="'Disable security-opt for non-administrators'"
|
||||
label-class="'col-sm-7 col-lg-4'"
|
||||
on-change="($ctrl.onChangeDisableSecurityOptForRegularUsers)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.isContainerEditDisabled()">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
|
|
|
@ -32,6 +32,19 @@
|
|||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<por-switch-field
|
||||
checked="formValues.enabled"
|
||||
name="'disableSecurityOptForRegularUsers'"
|
||||
label="'Enable pod security constraints'"
|
||||
feature-id="limitedFeaturePodSecurityPolicy"
|
||||
label-class="'col-sm-3 col-lg-2 px-0'"
|
||||
switch-class="'col-sm-8'"
|
||||
>
|
||||
</por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -103,6 +103,7 @@ export function createMockEnvironment(): Environment {
|
|||
allowHostNamespaceForRegularUsers: false,
|
||||
allowStackManagementForRegularUsers: false,
|
||||
allowSysctlSettingForRegularUsers: false,
|
||||
allowSecurityOptForRegularUsers: false,
|
||||
allowVolumeBrowserForRegularUsers: false,
|
||||
enableHostManagementFeatures: false,
|
||||
},
|
||||
|
|
|
@ -176,6 +176,11 @@ export function InnerForm({
|
|||
environment.SecuritySettings
|
||||
.allowSysctlSettingForRegularUsers
|
||||
}
|
||||
isSecurityOptFieldVisible={
|
||||
isEnvironmentAdminQuery.authorized ||
|
||||
environment.SecuritySettings
|
||||
.allowSecurityOptForRegularUsers
|
||||
}
|
||||
renderLimits={
|
||||
isDuplicate
|
||||
? (values) => (
|
||||
|
|
|
@ -13,6 +13,7 @@ import { GpuFieldset, GpuFieldsetValues } from './GpuFieldset';
|
|||
import { Values as RuntimeValues, RuntimeSection } from './RuntimeSection';
|
||||
import { DevicesField, Values as Devices } from './DevicesField';
|
||||
import { SysctlsField, Values as Sysctls } from './SysctlsField';
|
||||
import { SecurityOptField, Values as SecurityOpt } from './SecurityOptField';
|
||||
import {
|
||||
ResourceFieldset,
|
||||
Values as ResourcesValues,
|
||||
|
@ -24,6 +25,7 @@ export interface Values {
|
|||
devices: Devices;
|
||||
|
||||
sysctls: Sysctls;
|
||||
securityOpt: SecurityOpt;
|
||||
|
||||
sharedMemorySize: number;
|
||||
|
||||
|
@ -40,6 +42,7 @@ export function ResourcesTab({
|
|||
isInitFieldVisible,
|
||||
isDevicesFieldVisible,
|
||||
isSysctlFieldVisible,
|
||||
isSecurityOptFieldVisible,
|
||||
renderLimits,
|
||||
}: {
|
||||
values: Values;
|
||||
|
@ -49,6 +52,7 @@ export function ResourcesTab({
|
|||
isInitFieldVisible: boolean;
|
||||
isDevicesFieldVisible: boolean;
|
||||
isSysctlFieldVisible: boolean;
|
||||
isSecurityOptFieldVisible: boolean;
|
||||
renderLimits?: (values: ResourcesValues) => ReactNode;
|
||||
}) {
|
||||
const environmentId = useEnvironmentId();
|
||||
|
@ -88,6 +92,13 @@ export function ResourcesTab({
|
|||
/>
|
||||
)}
|
||||
|
||||
{isSecurityOptFieldVisible && (
|
||||
<SecurityOptField
|
||||
values={values.securityOpt}
|
||||
onChange={(securityOpt) => setFieldValue('securityOpt', securityOpt)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormControl label="Shared memory size" inputId="shm-size">
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { FormikErrors } from 'formik';
|
||||
import { array, SchemaOf, string } from 'yup';
|
||||
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { InputList, ItemProps } from '@@/form-components/InputList';
|
||||
import { InputLabeled } from '@@/form-components/Input/InputLabeled';
|
||||
|
||||
export type Values = Array<string>;
|
||||
|
||||
export function SecurityOptField({
|
||||
values,
|
||||
onChange,
|
||||
errors,
|
||||
}: {
|
||||
values: Values;
|
||||
onChange: (value: Values) => void;
|
||||
errors?: FormikErrors<string>[];
|
||||
}) {
|
||||
return (
|
||||
<InputList
|
||||
value={values}
|
||||
onChange={onChange}
|
||||
item={Item}
|
||||
addLabel="Add security-opt"
|
||||
label="SecurityOpt"
|
||||
errors={errors}
|
||||
itemBuilder={() => ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Item({ item, onChange, error }: ItemProps<string>) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex w-full gap-4">
|
||||
<InputLabeled
|
||||
value={item}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
label="Security Option"
|
||||
placeholder="e.g. seccomp=unconfined"
|
||||
className="w-full"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
{error && <FormError>{Object.values(error)[0]}</FormError>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function securityOptValidation(): SchemaOf<Values> {
|
||||
return array(
|
||||
string().required('Security option is required')
|
||||
);
|
||||
}
|
|
@ -24,6 +24,7 @@ export function toRequest(
|
|||
Sysctls: Object.fromEntries(
|
||||
values.sysctls.map((sysctl) => [sysctl.name, sysctl.value])
|
||||
),
|
||||
SecurityOpt: values.securityOpt,
|
||||
ShmSize: toConfigMemory(values.sharedMemorySize),
|
||||
DeviceRequests: gpuFieldsetUtils.toRequest(
|
||||
oldConfig.HostConfig.DeviceRequests || [],
|
||||
|
|
|
@ -19,6 +19,7 @@ export function toViewModel(config: ContainerDetailsJSON): Values {
|
|||
value,
|
||||
})
|
||||
),
|
||||
securityOpt: config.HostConfig?.SecurityOpt || [],
|
||||
gpu: gpuFieldsetUtils.toViewModel(config.HostConfig?.DeviceRequests || []),
|
||||
sharedMemorySize: toViewModelMemory(config.HostConfig?.ShmSize),
|
||||
resources: {
|
||||
|
@ -38,6 +39,7 @@ export function getDefaultViewModel(): Values {
|
|||
},
|
||||
devices: [],
|
||||
sysctls: [],
|
||||
securityOpt: [],
|
||||
sharedMemorySize: 64,
|
||||
gpu: gpuFieldsetUtils.getDefaultViewModel(),
|
||||
resources: {
|
||||
|
|
|
@ -6,6 +6,7 @@ import { resourcesValidation } from './ResourcesFieldset';
|
|||
import { Values } from './ResourcesTab';
|
||||
import { runtimeValidation } from './RuntimeSection';
|
||||
import { sysctlsValidation } from './SysctlsField';
|
||||
import { securityOptValidation } from './SecurityOptField';
|
||||
|
||||
export function validation({
|
||||
maxMemory,
|
||||
|
@ -18,6 +19,7 @@ export function validation({
|
|||
runtime: runtimeValidation(),
|
||||
devices: devicesValidation(),
|
||||
sysctls: sysctlsValidation(),
|
||||
securityOpt: securityOptValidation(),
|
||||
sharedMemorySize: number().min(0).default(0),
|
||||
gpu: gpuFieldsetUtils.validation(),
|
||||
resources: resourcesValidation({ maxMemory, maxCpu }),
|
||||
|
|
|
@ -108,6 +108,8 @@ export interface EnvironmentSecuritySettings {
|
|||
allowContainerCapabilitiesForRegularUsers: boolean;
|
||||
// Whether non-administrator should be able to use sysctl settings
|
||||
allowSysctlSettingForRegularUsers: boolean;
|
||||
// Whether non-administrator should be able to use security-opt settings
|
||||
allowSecurityOptForRegularUsers: boolean;
|
||||
// Whether host management features are enabled
|
||||
enableHostManagementFeatures: boolean;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue