feat(containers): prevent non-admin users from running containers using the host namespace pid (#3970)

* feat(containers): Prevent non-admin users from running containers using the host namespace pid

* feat(containers): add rbac check for swarm stack too

* feat(containers): remove forgotten conflict

* feat(containers): init EnableHostNamespaceUse to true and return 403 on forbidden action

* feat(containers): change enableHostNamespaceUse to restrictHostNamespaceUse in html

* feat(settings): rename EnableHostNamespaceUse to AllowHostNamespaceForRegularUsers
This commit is contained in:
Maxime Bajeux
2020-07-07 23:48:34 +02:00
committed by GitHub
parent b0ad212858
commit 0f58ece899
13 changed files with 95 additions and 30 deletions

View File

@@ -9,7 +9,7 @@ import (
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/cron"
@@ -274,6 +274,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowHostNamespaceForRegularUsers: true,
SnapshotInterval: *flags.SnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
}

View File

@@ -6,7 +6,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
type publicSettingsResponse struct {
@@ -20,6 +20,7 @@ type publicSettingsResponse struct {
ExternalTemplates bool `json:"ExternalTemplates"`
OAuthLoginURI string `json:"OAuthLoginURI"`
DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
}
// GET request on /api/settings/public
@@ -37,6 +38,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
ExternalTemplates: false,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
settings.OAuthSettings.AuthorizationURI,

View File

@@ -7,7 +7,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
@@ -26,6 +26,7 @@ type settingsUpdatePayload struct {
EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
DisableStackManagementForRegularUsers *bool
AllowHostNamespaceForRegularUsers *bool
}
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@@ -119,6 +120,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.DisableStackManagementForRegularUsers = *payload.DisableStackManagementForRegularUsers
}
if payload.AllowHostNamespaceForRegularUsers != nil {
settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
}
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
if err != nil {

View File

@@ -1,7 +1,6 @@
package stacks
import (
"errors"
"net/http"
"path"
"regexp"
@@ -331,29 +330,12 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
return err
}
rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err != nil && err != portainer.ErrObjectNotFound {
return errors.New("Unable to verify if RBAC extension is loaded")
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
endpointResourceAccess := false
_, ok := config.user.EndpointAuthorizations[portainer.EndpointID(config.endpoint.ID)][portainer.EndpointResourcesAccess]
if ok {
endpointResourceAccess = true
}
mustBeChecked := false
if rbacExtension != nil {
if !config.isAdmin && !endpointResourceAccess {
mustBeChecked = true
}
} else {
if !config.isAdmin {
mustBeChecked = true
}
}
if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers) && mustBeChecked {
if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers) && !isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)

View File

@@ -291,6 +291,7 @@ type swarmStackDeploymentConfig struct {
registries []portainer.Registry
prune bool
isAdmin bool
user *portainer.User
}
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
@@ -310,6 +311,11 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
}
filteredRegistries := security.FilterRegistries(registries, securityContext)
user, err := handler.UserService.User(securityContext.UserID)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
}
config := &swarmStackDeploymentConfig{
stack: stack,
endpoint: endpoint,
@@ -317,6 +323,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
registries: filteredRegistries,
prune: prune,
isAdmin: securityContext.IsAdmin,
user: user,
}
return config, nil
@@ -328,7 +335,12 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)

View File

@@ -1,12 +1,13 @@
package stacks
import (
"errors"
"net/http"
"sync"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
@@ -111,3 +112,30 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
}
return false, nil
}
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
isAdmin := user.Role == portainer.AdministratorRole
rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err != nil && err != portainer.ErrObjectNotFound {
return false, errors.New("Unable to verify if RBAC extension is loaded")
}
endpointResourceAccess := false
_, ok := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess]
if ok {
endpointResourceAccess = true
}
if rbacExtension != nil {
if isAdmin || endpointResourceAccess {
return true, nil
}
} else {
if isAdmin {
return true, nil
}
}
return false, nil
}

View File

@@ -156,6 +156,10 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
return errors.New("privileged mode disabled for non administrator users")
}
if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
return errors.New("pid host disabled for non administrator users")
}
}
return nil

View File

@@ -156,10 +156,15 @@ 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"`
Privileged bool `json:"Privileged"`
PidMode string `json:"PidMode"`
} `json:"HostConfig"`
}
forbiddenResponse := &http.Response{
StatusCode: http.StatusForbidden,
}
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
@@ -188,7 +193,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
return nil, err
}
if !settings.AllowPrivilegedModeForRegularUsers {
if !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers {
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return nil, err
@@ -201,7 +206,11 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
}
if partialContainer.HostConfig.Privileged {
return nil, errors.New("forbidden to use privileged mode")
return forbiddenResponse, errors.New("forbidden to use privileged mode")
}
if partialContainer.HostConfig.PidMode == "host" {
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
}
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))

View File

@@ -434,6 +434,7 @@ type (
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
// Deprecated fields
DisplayDonationHeader bool