runtime: provide credentials to services (#1817)

* runtime: inject credentials into service

* util/auth: self generate accounts (needed for jwt)

* runtime/kubernetes: add logging for creds

* runtime/kubernetes: serialize secret name

* runtime/kubernetes: remove unused code

* runtime/kubernetes: base64 encode secret

* runtime/kubernetes: remove metadata from secret

* util/kubernetes/client: omit empty secret metadata

* util/kubernetes/client: fix secret template

* util/kubernetes/client: fix secrets

* web: update auth util

* util/auth: fix missing arg

* extend token expiry

* extend token expiry
This commit is contained in:
ben-toogood 2020-07-10 16:25:46 +01:00 committed by GitHub
parent 3480e0a64e
commit 09ec20fded
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 128 additions and 23 deletions

View File

@ -51,17 +51,21 @@ func (j *jwt) Options() auth.Options {
func (j *jwt) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) {
options := auth.NewGenerateOptions(opts...)
if len(options.Issuer) == 0 {
options.Issuer = j.Options().Issuer
}
account := &auth.Account{
ID: id,
Type: options.Type,
Scopes: options.Scopes,
Metadata: options.Metadata,
Issuer: j.Options().Issuer,
Issuer: options.Issuer,
}
// generate a JWT secret which can be provided to the Token() method
// and exchanged for an access token
secret, err := j.jwt.Generate(account)
secret, err := j.jwt.Generate(account, token.WithExpiry(time.Hour*24*365))
if err != nil {
return nil, err
}

View File

@ -130,6 +130,8 @@ type GenerateOptions struct {
Type string
// Secret used to authenticate the account
Secret string
// Issuer of the account, e.g. micro
Issuer string
}
type GenerateOption func(o *GenerateOptions)
@ -169,6 +171,13 @@ func WithScopes(s ...string) GenerateOption {
}
}
// WithIssuer for the generated account
func WithIssuer(i string) GenerateOption {
return func(o *GenerateOptions) {
o.Issuer = i
}
}
// NewGenerateOptions from a slice of options
func NewGenerateOptions(opts ...GenerateOption) GenerateOptions {
var options GenerateOptions

View File

@ -667,10 +667,8 @@ func (c *cmd) Before(ctx *cli.Context) error {
(*c.opts.Auth).Init(authOpts...)
}
// generate the services auth account.
// todo: move this so it only runs for new services
serverID := (*c.opts.Server).Options().Id
if err := authutil.Generate(serverID, c.App().Name, (*c.opts.Auth)); err != nil {
// verify the auth's service account
if err := authutil.Verify(*c.opts.Auth); err != nil {
return err
}

View File

@ -269,6 +269,18 @@ func (r *runtime) Create(s *Service, opts ...CreateOption) error {
options.Args = []string{"run", "."}
}
// pass credentials as env vars
if len(options.Credentials) > 0 {
// validate the creds
comps := strings.Split(options.Credentials, ":")
if len(comps) != 2 {
return errors.New("Invalid credentials, expected format 'user:pass'")
}
options.Env = append(options.Env, "MICRO_AUTH_ID", comps[0])
options.Env = append(options.Env, "MICRO_AUTH_SECRET", comps[1])
}
if _, ok := r.namespaces[options.Namespace]; !ok {
r.namespaces[options.Namespace] = make(map[string]*service)
}

View File

@ -2,10 +2,14 @@
package kubernetes
import (
"encoding/base64"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/micro/go-micro/v2/logger"
log "github.com/micro/go-micro/v2/logger"
"github.com/micro/go-micro/v2/runtime"
"github.com/micro/go-micro/v2/util/kubernetes/client"
@ -420,10 +424,27 @@ func (k *kubernetes) Create(s *runtime.Service, opts ...runtime.CreateOption) er
return err
}
}
// determine the image from the source and options
options.Image = k.getImage(s, options)
// create a secret for the credentials if some where provided
if len(options.Credentials) > 0 {
secret, err := k.createCredentials(s, options)
if err != nil {
if logger.V(logger.WarnLevel, logger.DefaultLogger) {
logger.Warnf("Error generating auth credentials for service: %v", err)
}
return err
}
if logger.V(logger.DebugLevel, logger.DefaultLogger) {
logger.Debugf("Generated auth credentials for service %v", s.Name)
}
// pass the secret name to the client via the credentials option
options.Credentials = secret
}
// create new service
service := newService(s, options)
@ -530,7 +551,6 @@ func (k *kubernetes) Delete(s *runtime.Service, opts ...runtime.DeleteOption) er
options := runtime.DeleteOptions{
Namespace: client.DefaultNamespace,
}
for _, o := range opts {
o(&options)
}
@ -544,7 +564,11 @@ func (k *kubernetes) Delete(s *runtime.Service, opts ...runtime.DeleteOption) er
Namespace: options.Namespace,
})
return service.Stop(k.client, client.DeleteNamespace(options.Namespace))
// delete the service credentials
ns := client.DeleteNamespace(options.Namespace)
k.client.Delete(&client.Resource{Name: credentialsName(s), Kind: "secret"}, ns)
return service.Stop(k.client, ns)
}
// Start starts the runtime
@ -643,3 +667,36 @@ func (k *kubernetes) getImage(s *runtime.Service, options runtime.CreateOptions)
return ""
}
func (k *kubernetes) createCredentials(service *runtime.Service, options runtime.CreateOptions) (string, error) {
// validate the creds
comps := strings.Split(options.Credentials, ":")
if len(comps) != 2 {
return "", errors.New("Invalid credentials, expected format 'user:pass'")
}
// construct the k8s secret object
secret := &client.Secret{
Type: "Opaque",
Data: map[string]string{
"id": base64.StdEncoding.EncodeToString([]byte(comps[0])),
"secret": base64.StdEncoding.EncodeToString([]byte(comps[1])),
},
Metadata: &client.Metadata{
Name: credentialsName(service),
Namespace: options.Namespace,
},
}
// create options specify the namespace
ns := client.CreateNamespace(options.Namespace)
// crete the secret in kubernetes
name := credentialsName(service)
err := k.client.Create(&client.Resource{Kind: "secret", Name: name, Value: secret}, ns)
return name, err
}
func credentialsName(service *runtime.Service) string {
name := fmt.Sprintf("%v-%v-credentials", service.Name, service.Version)
return client.SerializeResourceName(name)
}

View File

@ -75,6 +75,27 @@ func newService(s *runtime.Service, c runtime.CreateOptions) *service {
env = append(env, client.EnvVar{Name: evarPair[0], Value: evarPair[1]})
}
// if credentials were provided, pass them to the service
if len(c.Credentials) > 0 {
env = append(env, client.EnvVar{
Name: "MICRO_AUTH_ID",
ValueFrom: &client.EnvVarSource{
SecretKeyRef: &client.SecretKeySelector{
Name: c.Credentials, Key: "id",
},
},
})
env = append(env, client.EnvVar{
Name: "MICRO_AUTH_SECRET",
ValueFrom: &client.EnvVarSource{
SecretKeyRef: &client.SecretKeySelector{
Name: c.Credentials, Key: "secret",
},
},
})
}
// if environment has been supplied update deployment default environment
if len(env) > 0 {
kdeploy.Spec.Template.PodSpec.Containers[0].Env = append(kdeploy.Spec.Template.PodSpec.Containers[0].Env, env...)

View File

@ -82,6 +82,8 @@ type CreateOptions struct {
Namespace string
// Specify the context to use
Context context.Context
// Credentials for the service to use
Credentials string
}
// ReadOptions queries runtime services
@ -126,6 +128,13 @@ func CreateContext(ctx context.Context) CreateOption {
}
}
// CreateCredentials sets the credentials to start the service with
func CreateCredentials(user, pass string) CreateOption {
return func(o *CreateOptions) {
o.Credentials = user + ":" + pass
}
}
// WithCommand specifies the command to execute
func WithCommand(cmd ...string) CreateOption {
return func(o *CreateOptions) {

View File

@ -1,34 +1,31 @@
package auth
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/micro/go-micro/v2/auth"
"github.com/micro/go-micro/v2/logger"
)
// Generate generates a service account for and continually
// refreshes the access token.
func Generate(id, name string, a auth.Auth) error {
// Verify the auth credentials and refresh the auth token periodicallay
func Verify(a auth.Auth) error {
// extract the account creds from options, these can be set by flags
accID := a.Options().ID
accSecret := a.Options().Secret
// if no credentials were provided, generate an account
if len(accID) == 0 || len(accSecret) == 0 {
name := fmt.Sprintf("%v-%v", name, id)
// if no credentials were provided, self generate an account
if len(accID) == 0 && len(accSecret) == 0 {
opts := []auth.GenerateOption{
auth.WithType("service"),
auth.WithScopes("service"),
}
acc, err := a.Generate(name, opts...)
acc, err := a.Generate(uuid.New().String(), opts...)
if err != nil {
return err
}
logger.Debugf("Auth [%v] Authenticated as %v issued by %v", a, name, acc.Issuer)
logger.Debugf("Auth [%v] Self-generated an auth account", a.String())
accID = acc.ID
accSecret = acc.Secret

View File

@ -212,7 +212,7 @@ type ImagePullSecret struct {
type Secret struct {
Type string `json:"type,omitempty"`
Data map[string]string `json:"data"`
Metadata *Metadata `json:"metadata"`
Metadata *Metadata `json:"metadata,omitempty"`
}
// ServiceAccount

View File

@ -449,9 +449,7 @@ func (s *service) Init(opts ...Option) error {
func (s *service) Run() error {
// generate an auth account
srvID := s.opts.Service.Server().Options().Id
srvName := s.Options().Name
if err := authutil.Generate(srvID, srvName, s.opts.Service.Options().Auth); err != nil {
if err := authutil.Verify(s.opts.Service.Options().Auth); err != nil {
return err
}