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 <oskirby@gmail.com>
pull/4982/head
Lukas Grotz 2021-04-12 09:40:45 +02:00 committed by GitHub
parent ac7d819620
commit d09ae22ba8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 125 additions and 9 deletions

View File

@ -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
}

View File

@ -356,6 +356,7 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
!securitySettings.AllowPrivilegedModeForRegularUsers ||
!securitySettings.AllowHostNamespaceForRegularUsers ||
!securitySettings.AllowDeviceMappingForRegularUsers ||
!securitySettings.AllowSysctlSettingForRegularUsers ||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
!isAdminOrEndpointAdmin {

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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"`
}

View File

@ -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();
}

View File

@ -706,6 +706,33 @@
<!-- !devices-input-list -->
</div>
<!-- !devices-->
<!-- sysctls -->
<div ng-if="showSysctls" class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Sysctls</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSysctl()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add sysctl
</span>
</div>
<!-- sysctls-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="sysctl in formValues.Sysctls" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="sysctl.name" placeholder="e.g. FOO" />
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="sysctl.value" placeholder="e.g. bar" />
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSysctl($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !sysctls-input-list -->
</div>
<!-- !sysctls -->
<div class="col-sm-12 form-section-title">
Resources
</div>

View File

@ -274,6 +274,17 @@
</container-restart-policy>
</td>
</tr>
<tr ng-if="!(container.HostConfig.Sysctls | emptyobject)">
<td>Sysctls</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in container.HostConfig.Sysctls">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>

View File

@ -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;

View File

@ -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,
};
}
}

View File

@ -108,6 +108,16 @@
></por-switch-field>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<por-switch-field
ng-model="$ctrl.formValues.disableSysctlSettingForRegularUsers"
name="disableSysctlSettingForRegularUsers"
label="Disable sysctl settings for non-administrators"
label-class="col-sm-7 col-lg-4"
></por-switch-field>
</div>
</div>
<div class="form-group" ng-if="$ctrl.isContainerEditDisabled()">
<span class="col-sm-12 text-muted small">

View File

@ -53,7 +53,11 @@
<div
class="col-sm-11 small text-warning"
style="margin-top: 5px;"
ng-show="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid || (!entry.Used && $ctrl.state.duplicateKeys[index] !== undefined) || $ctrl.state.invalidKeys[index]"
ng-show="
kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$invalid ||
(!entry.Used && $ctrl.state.duplicateKeys[index] !== undefined) ||
$ctrl.state.invalidKeys[index]
"
>
<ng-messages for="kubernetesConfigurationDataCreationForm['configuration_data_key_' + index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>

View File

@ -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');

View File

@ -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' }];