MCS service account authentication with Mkube (#166)
`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 ````
This commit is contained in:
@@ -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"
|
||||
@@ -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
pkg/auth/mkube.go
Normal file
77
pkg/auth/mkube.go
Normal file
@@ -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
|
||||
}
|
||||
117
pkg/auth/mkube/config.go
Normal file
117
pkg/auth/mkube/config.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// 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 mkube
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/env"
|
||||
)
|
||||
|
||||
var (
|
||||
certDontExists = "File certificate doesn't exists: %s"
|
||||
)
|
||||
|
||||
// getMkubeEndpoint returns the hostname of mkube
|
||||
func GetMkubeEndpoint() string {
|
||||
return env.Get(McsMkubeHost, "http://m3:8787")
|
||||
}
|
||||
|
||||
// getMkubeEndpointIsSecure returns true or false depending on the protocol in Mkube URL
|
||||
func getMkubeEndpointIsSecure() bool {
|
||||
server := GetMkubeEndpoint()
|
||||
if strings.Contains(server, "://") {
|
||||
parts := strings.Split(server, "://")
|
||||
if len(parts) > 1 {
|
||||
if parts[0] == "https" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// If MCS_M3_SERVER_TLS_CA_CERTIFICATE is true mcs will load a list of certificates into the
|
||||
// http.client rootCAs store, this is useful for testing or when working with self-signed certificates
|
||||
func getMkubeServerTLSRootCAs() []string {
|
||||
caCertFileNames := strings.TrimSpace(env.Get(McsMkubeTLSCACertificate, ""))
|
||||
if caCertFileNames == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(caCertFileNames, ",")
|
||||
}
|
||||
|
||||
// FileExists verifies if a file exist on the desired location and its not a folder
|
||||
func FileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
// GetMkubeHTTPClient returns an http.Client with custom configurations used by MCS to talk to Mkube
|
||||
// custom configurations include the use of CA certificates
|
||||
func getMkubeHTTPClient() *http.Client {
|
||||
httpTransport := &http.Transport{}
|
||||
// If Mkube server is running with TLS enabled and it's using a self-signed certificate
|
||||
// or a certificate issued by a custom certificate authority we prepare a new custom *http.Transport
|
||||
if getMkubeEndpointIsSecure() {
|
||||
caCertFileNames := getMkubeServerTLSRootCAs()
|
||||
tlsConfig := &tls.Config{
|
||||
// Can't use SSLv3 because of POODLE and BEAST
|
||||
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
|
||||
// Can't use TLSv1.1 because of RC4 cipher usage
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
// If CAs certificates are configured we save them to the http.Client RootCAs store
|
||||
if len(caCertFileNames) > 0 {
|
||||
certs := x509.NewCertPool()
|
||||
for _, caCert := range caCertFileNames {
|
||||
// Validate certificate exists
|
||||
if FileExists(caCert) {
|
||||
pemData, err := ioutil.ReadFile(caCert)
|
||||
if err != nil {
|
||||
// if there was an error reading pem file stop mcs
|
||||
panic(err)
|
||||
}
|
||||
certs.AppendCertsFromPEM(pemData)
|
||||
} else {
|
||||
// if provided cert filename doesn't exists stop mcs
|
||||
panic(fmt.Sprintf(certDontExists, caCert))
|
||||
}
|
||||
}
|
||||
tlsConfig.RootCAs = certs
|
||||
}
|
||||
httpTransport.TLSClientConfig = tlsConfig
|
||||
}
|
||||
|
||||
// Return http client with default configuration
|
||||
return &http.Client{
|
||||
Transport: httpTransport,
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPClient it's a public variable that contains the HTTP configuration to be used by MCS to talk to Mkube
|
||||
// This function will run only once
|
||||
var HTTPClient = getMkubeHTTPClient()
|
||||
22
pkg/auth/mkube/const.go
Normal file
22
pkg/auth/mkube/const.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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 mkube
|
||||
|
||||
const (
|
||||
McsMkubeHost = "MCS_M3_HOSTNAME"
|
||||
McsMkubeTLSCACertificate = "MCS_M3_SERVER_TLS_CA_CERTIFICATE"
|
||||
)
|
||||
77
pkg/auth/mkube_test.go
Normal file
77
pkg/auth/mkube_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// RoundTripFunc .
|
||||
type RoundTripFunc func(req *http.Request) (*http.Response, error)
|
||||
|
||||
// RoundTrip .
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
//NewTestClient returns *http.Client with Transport replaced to avoid making real calls
|
||||
func NewTestClient(fn RoundTripFunc) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: fn,
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isServiceAccountTokenValid(t *testing.T) {
|
||||
|
||||
successResponse := NewTestClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(`OK`)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})
|
||||
|
||||
failResponse := NewTestClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 500,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(`NOTOK`)),
|
||||
Header: make(http.Header),
|
||||
}, errors.New("something wrong")
|
||||
})
|
||||
|
||||
type args struct {
|
||||
client *http.Client
|
||||
jwt string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "Success authentication - correct jwt (service account token)",
|
||||
args: args{
|
||||
client: successResponse,
|
||||
jwt: "GOODTOKEN",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Fail authentication - incorrect jwt (service account token)",
|
||||
args: args{
|
||||
client: failResponse,
|
||||
jwt: "BADTOKEN",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isServiceAccountTokenValid(tt.args.client, tt.args.jwt); got != tt.want {
|
||||
t.Errorf("isServiceAccountTokenValid() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user