Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/mcs_service_account_mkube.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# MCS service account authentication with Mkube

`MCS` will authenticate against `Mkube`using bearer tokens via HTTP `Authorization` header. The user will provide this token once
in the login form, MCS will validate it against Mkube (list tenants) and if valid will generate and return a new MCS sessions
with encrypted claims (the user Service account token will be inside the JWT in the data field)

# Kubernetes

The provided `JWT token` corresponds to the `Kubernetes service account` that `Mkube` will use to run tasks on behalf of the
user, ie: list, create, edit, delete tenants, storage class, etc.

# Development

If you are running mcs in your local environment and wish to make request to `Mkube` you can set `MCS_M3_HOSTNAME`, if
the environment variable is not present by default `MCS` will use `"http://m3:8787"`, additionally you will need to set the
`MCS_MKUBE_ADMIN_ONLY=on` variable to make MCS display the Mkube UI

## Extract the Service account token and use it with MCS

For local development you can use the jwt associated to the `m3-sa` service account, you can get the token running
the following command in your terminal:

```
kubectl get secret $(kubectl get serviceaccount m3-sa -o jsonpath="{.secrets[0].name}") -o jsonpath="{.data.token}" | base64 --decode
```

Then run the mcs server

```
MCS_M3_HOSTNAME=http://localhost:8787 MCS_MKUBE_ADMIN_ONLY=on ./mcs server
```

# Self-signed certificates and Custom certificate authority for Mkube

If Mkube uses TLS with a self-signed certificate, or a certificate issued by a custom certificate authority you can add those
certificates usinng the `MCS_M3_SERVER_TLS_CA_CERTIFICATE` env variable

````
MCS_M3_SERVER_TLS_CA_CERTIFICATE=cert1.pem,cert2.pem,cert3.pem ./mcs server
````
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ require (
github.com/json-iterator/go v1.1.9
github.com/minio/cli v1.22.0
github.com/minio/mc v0.0.0-20200515235434-3b479cf92ed6
github.com/minio/minio v0.0.0-20200516011754-9cac385aecdb
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f
github.com/minio/minio v0.0.0-20200603201854-5686a7e27319
github.com/minio/minio-go/v6 v6.0.56
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.5.1
Expand Down
50 changes: 45 additions & 5 deletions go.sum

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions models/login_details.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions models/login_mkube_request.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions pkg/acl/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import (
"github.com/minio/minio/pkg/env"
)

// GetOperatorOnly gets mcs operator mode status set on env variable
//or default one
func GetOperatorOnly() string {
return strings.ToLower(env.Get(McsOperatorOnly, "off"))
// GetOperatorOnly gets MCS mkube admin mode status set on env variable
// or default one
func GetOperatorOnly() bool {
return strings.ToLower(env.Get(McsmKubeAdminOnly, "off")) == "on"
}
2 changes: 1 addition & 1 deletion pkg/acl/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
package acl

const (
McsOperatorOnly = "MCS_OPERATOR_ONLY"
McsmKubeAdminOnly = "MCS_MKUBE_ADMIN_ONLY"
)
2 changes: 1 addition & 1 deletion pkg/acl/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func actionsStringToActionSet(actions []string) iampolicy.ActionSet {
func GetAuthorizedEndpoints(actions []string) []string {
rangeTake := endpointRules

if operatorOnly == "on" {
if operatorOnly {
rangeTake = operatorRules
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/acl/endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
}

func TestOperatorOnlyEndpoints(t *testing.T) {
operatorOnly = "on"
operatorOnly = true

tests := []endpoint{
{
Expand Down
41 changes: 41 additions & 0 deletions pkg/auth/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import (
"fmt"
"io"
"log"
"net/http"
"strings"

jwtgo "github.com/dgrijalva/jwt-go"
"github.com/go-openapi/swag"
xjwt "github.com/minio/mcs/pkg/auth/jwt"
"github.com/minio/minio-go/v6/pkg/credentials"
"github.com/minio/minio/cmd"
Expand Down Expand Up @@ -182,3 +184,42 @@ func decrypt(data []byte) ([]byte, error) {
}
return plaintext, nil
}

// GetTokenFromRequest returns a token from a http Request
// either defined on a cookie `token` or on Authorization header.
//
// Authorization Header needs to be like "Authorization Bearer <jwt_token>"
func GetTokenFromRequest(r *http.Request) (*string, error) {
// Get Auth token
var reqToken string

// Token might come either as a Cookie or as a Header
// if not set in cookie, check if it is set on Header.
tokenCookie, err := r.Cookie("token")
if err != nil {
headerToken := r.Header.Get("Authorization")
// reqToken should come as "Bearer <token>"
splitHeaderToken := strings.Split(headerToken, "Bearer")
if len(splitHeaderToken) <= 1 {
return nil, errNoAuthToken
}
reqToken = strings.TrimSpace(splitHeaderToken[1])
} else {
reqToken = strings.TrimSpace(tokenCookie.Value)
}
return swag.String(reqToken), nil
}

func GetClaimsFromTokenInRequest(req *http.Request) (*DecryptedClaims, error) {
sessionID, err := GetTokenFromRequest(req)
if err != nil {
return nil, err
}
// Perform decryption of the JWT, if MCS is able to decrypt the JWT that means a valid session
// was used in the first place to get it
claims, err := JWTAuthenticate(*sessionID)
if err != nil {
return nil, err
}
return claims, nil
}
77 changes: 77 additions & 0 deletions pkg/auth/mkube.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package auth

import (
"fmt"
"log"
"net/http"

"github.com/minio/mcs/pkg/auth/mkube"
"github.com/minio/minio-go/v6/pkg/credentials"
)

// mkubeCredentialsProvider is an struct to hold the JWT (service account token)
type mkubeCredentialsProvider struct {
serviceAccountJWT string
}

// Implementing the interfaces of the minio Provider, we use this to leverage on the existing mcs Authentication flow
func (s mkubeCredentialsProvider) Retrieve() (credentials.Value, error) {
return credentials.Value{
AccessKeyID: "",
SecretAccessKey: "",
SessionToken: s.serviceAccountJWT,
}, nil
}

// IsExpired dummy function, must be implemented in order to work with the minio provider authentication
func (s mkubeCredentialsProvider) IsExpired() bool {
return false
}

// isServiceAccountTokenValid will make an authenticated request (using bearer token) against Mkube hostname, if the
// request success means the provided jwt its a valid service account token and the MCS user can use it for future requests
// until it fails
func isServiceAccountTokenValid(client *http.Client, jwt string) bool {
url := fmt.Sprintf("%s/api/v1/tenants", mkube.GetMkubeEndpoint())
m3Req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Println(err)
return false
}
token := fmt.Sprintf("Bearer %s", jwt)
m3Req.Header.Add("Authorization", token)
resp, err := client.Do(m3Req)
if err != nil {
log.Println(err)
return false
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return true
}
return false
}

// GetMcsCredentialsFromMkube will validate the provided JWT (service account token) and return it in the form of credentials.Credentials
func GetMcsCredentialsFromMkube(jwt string) (*credentials.Credentials, error) {
if isServiceAccountTokenValid(mkube.HTTPClient, jwt) {
return credentials.New(mkubeCredentialsProvider{serviceAccountJWT: jwt}), nil
}
return nil, errInvalidCredentials
}
Loading