From b5ff74862c3a8fcd09be6763472289e006bda5c2 Mon Sep 17 00:00:00 2001 From: Tamir Zheleznyak <73157235+tzheleznyak@users.noreply.github.com> Date: Thu, 23 Dec 2021 15:18:34 +0200 Subject: [PATCH] ONYX-14719: Refactor authn-k8s-client to be authentication flow generic --- cmd/authenticator/main.go | 14 +- pkg/authenticator/authenticator.go | 428 +-------------- pkg/authenticator/authenticator_factory.go | 33 ++ pkg/authenticator/{ => common}/client.go | 5 +- pkg/authenticator/common/common_config.go | 58 ++ .../{config => common}/username.go | 2 +- .../{config => common}/username_test.go | 7 +- pkg/authenticator/common/validations.go | 72 +++ pkg/authenticator/config/config.go | 293 ---------- pkg/authenticator/config/config_test.go | 501 ------------------ pkg/authenticator/config/configuration.go | 17 + .../config/configuration_factory.go | 123 +++++ pkg/authenticator/config/validate_test.go | 185 +++++++ pkg/authenticator/k8s/authenticator.go | 419 +++++++++++++++ pkg/authenticator/k8s/config.go | 133 +++++ pkg/authenticator/{ => k8s}/requests.go | 2 +- pkg/authenticator/{ => k8s}/requests_test.go | 2 +- .../{ => k8s/tests}/authenticator_test.go | 51 +- .../tests}/authenticator_test_server.go | 2 +- pkg/authenticator/k8s/tests/config_test.go | 171 ++++++ pkg/log/log_messages.go | 2 + 21 files changed, 1260 insertions(+), 1260 deletions(-) create mode 100644 pkg/authenticator/authenticator_factory.go rename pkg/authenticator/{ => common}/client.go (86%) create mode 100644 pkg/authenticator/common/common_config.go rename pkg/authenticator/{config => common}/username.go (99%) rename pkg/authenticator/{config => common}/username_test.go (89%) create mode 100644 pkg/authenticator/common/validations.go delete mode 100644 pkg/authenticator/config/config.go delete mode 100644 pkg/authenticator/config/config_test.go create mode 100644 pkg/authenticator/config/configuration.go create mode 100644 pkg/authenticator/config/configuration_factory.go create mode 100644 pkg/authenticator/config/validate_test.go create mode 100644 pkg/authenticator/k8s/authenticator.go create mode 100644 pkg/authenticator/k8s/config.go rename pkg/authenticator/{ => k8s}/requests.go (98%) rename pkg/authenticator/{ => k8s}/requests_test.go (95%) rename pkg/authenticator/{ => k8s/tests}/authenticator_test.go (76%) rename pkg/authenticator/{ => k8s/tests}/authenticator_test_server.go (99%) create mode 100644 pkg/authenticator/k8s/tests/config_test.go diff --git a/cmd/authenticator/main.go b/cmd/authenticator/main.go index 96e3845d..b3609cb1 100644 --- a/cmd/authenticator/main.go +++ b/cmd/authenticator/main.go @@ -3,13 +3,13 @@ package main import ( "context" "fmt" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" "os" "time" "github.com/cenkalti/backoff" - "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator" - authnConfig "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" "github.com/cyberark/conjur-authn-k8s-client/pkg/log" "github.com/cyberark/secrets-provider-for-k8s/pkg/trace" ) @@ -19,7 +19,7 @@ func main() { var err error - config, err := authnConfig.NewFromEnv() + config, err := config.NewConfigFromEnv() if err != nil { printErrorAndExit(log.CAKC018) } @@ -28,7 +28,7 @@ func main() { defer tracer.Shutdown(context.Background()) // Create new Authenticator - authn, err := authenticator.New(*config) + authn, err := authenticator.NewAuthenticator(config) if err != nil { printErrorAndExit(log.CAKC019) } @@ -48,14 +48,14 @@ func main() { return log.RecordedError(log.CAKC016) } - if authn.Config.ContainerMode == "init" { + if config.GetContainerMode() == "init" { os.Exit(0) } - log.Info(log.CAKC047, authn.Config.TokenRefreshTimeout) + log.Info(log.CAKC047, config.GetTokenTimeout()) fmt.Println() - time.Sleep(authn.Config.TokenRefreshTimeout) + time.Sleep(config.GetTokenTimeout()) // Reset exponential backoff expBackoff.Reset() diff --git a/pkg/authenticator/authenticator.go b/pkg/authenticator/authenticator.go index 09df45f9..1df4fd62 100644 --- a/pkg/authenticator/authenticator.go +++ b/pkg/authenticator/authenticator.go @@ -2,431 +2,9 @@ package authenticator import ( "context" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/pem" - "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" - "os" - "time" - - "github.com/fullsailor/pkcs7" - "go.opentelemetry.io/otel" - - "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token" - "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token/file" - authnConfig "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" - "github.com/cyberark/conjur-authn-k8s-client/pkg/log" - "github.com/cyberark/conjur-authn-k8s-client/pkg/utils" - "github.com/cyberark/secrets-provider-for-k8s/pkg/trace" ) -var oidExtensionSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17} -var bufferTime = 30 * time.Second - -// Authenticator contains the configuration and client -// for the authentication connection to Conjur -type Authenticator struct { - client *http.Client - privateKey *rsa.PrivateKey - AccessToken access_token.AccessToken - Config authnConfig.Config - PublicCert *x509.Certificate -} - -const ( - nameTypeEmail = 1 - nameTypeDNS = 2 - nameTypeURI = 6 - nameTypeIP = 7 -) - -// New creates a new authenticator instance from a token file -func New(config authnConfig.Config) (*Authenticator, error) { - accessToken, err := file.NewAccessToken(config.TokenFilePath) - if err != nil { - return nil, log.RecordedError(log.CAKC001) - } - - return NewWithAccessToken(config, accessToken) -} - -// NewWithAccessToken creates a new authenticator instance from a given access token -func NewWithAccessToken(config authnConfig.Config, accessToken access_token.AccessToken) (*Authenticator, error) { - signingKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, log.RecordedError(log.CAKC030, err) - } - - client, err := newHTTPSClient(config.SSLCertificate, nil, nil) - if err != nil { - return nil, err - } - - return &Authenticator{ - client: client, - privateKey: signingKey, - AccessToken: accessToken, - Config: config, - }, nil -} - -// Authenticate sends Conjur an authenticate request and writes the response -// to the token file (after decrypting it if needed). It also manages state of -// certificates. -// @deprecated Use AuthenticateWithContext instead -func (auth *Authenticator) Authenticate() error { - return auth.AuthenticateWithContext(context.TODO()) -} - -// Authenticate sends Conjur an authenticate request and writes the response -// to the token file (after decrypting it if needed). It also manages state of -// certificates. -func (auth *Authenticator) AuthenticateWithContext(ctx context.Context) error { - log.Info(log.CAKC040, auth.Config.Username) - - tr := trace.NewOtelTracer(otel.Tracer("conjur-authn-k8s-client")) - spanCtx, span := tr.Start(ctx, "Authenticate") - defer span.End() - - err := auth.loginIfNeeded(spanCtx, tr) - if err != nil { - span.RecordErrorAndSetStatus(err) - span.End() - return err - } - - authenticationResponse, err := auth.sendAuthenticationRequest(spanCtx, tr) - if err != nil { - span.RecordErrorAndSetStatus(err) - span.End() - return err - } - - parsedResponse, err := auth.parseAuthenticationResponse(spanCtx, tr, authenticationResponse) - if err != nil { - span.RecordErrorAndSetStatus(err) - span.End() - return err - } - - err = auth.AccessToken.Write(parsedResponse) - if err != nil { - return err - } - - log.Info(log.CAKC035) - return nil -} - -// generateCSR prepares the CSR -func (auth *Authenticator) generateCSR(commonName string) ([]byte, error) { - sanURIString, err := generateSANURI(auth.Config.PodNamespace, auth.Config.PodName) - sanURI, err := url.Parse(sanURIString) - if err != nil { - return nil, err - } - - subj := pkix.Name{ - CommonName: commonName, - } - - template := x509.CertificateRequest{ - Subject: subj, - SignatureAlgorithm: x509.SHA256WithRSA, - } - - subjectAltNamesValue, err := marshalSANs(nil, nil, nil, []*url.URL{ - sanURI, - }) - if err != nil { - return nil, err - } - - extSubjectAltName := pkix.Extension{ - Id: oidExtensionSubjectAltName, - Critical: false, - Value: subjectAltNamesValue, - } - template.ExtraExtensions = []pkix.Extension{extSubjectAltName} - - return x509.CreateCertificateRequest(rand.Reader, &template, auth.privateKey) -} - -// login sends Conjur a CSR and verifies that the client cert is -// successfully retrieved -func (auth *Authenticator) login(ctx context.Context, tracer trace.Tracer) error { - - log.Debug(log.CAKC041, auth.Config.Username) - - _, span := tracer.Start(ctx, "Generate CSR") - csrRawBytes, err := auth.generateCSR(auth.Config.Username.Suffix) - - csrBytes := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE REQUEST", Bytes: csrRawBytes, - }) - span.End() - - req, err := LoginRequest(auth.Config.URL, auth.Config.ConjurVersion, csrBytes, auth.Config.Username.Prefix) - if err != nil { - return err - } - - _, span = tracer.Start(ctx, "Send login request") - resp, err := auth.client.Do(req) - if err != nil { - span.RecordErrorAndSetStatus(err) - span.End() - return log.RecordedError(log.CAKC028, err) - } - span.End() - - err = utils.ValidateResponse(resp) - if err != nil { - return log.RecordedError(log.CAKC029, err) - } - - _, span = tracer.Start(ctx, "Wait for cert file") - // Ensure client certificate exists before attempting to read it, with a tolerance - // for small delays - err = utils.WaitForFile( - auth.Config.ClientCertPath, - auth.Config.ClientCertRetryCountLimit, - ) - if err != nil { - // The response code was changed from 200 to 202 in the same Conjur version - // that started writing the cert injection logs to the client. Verifying that - // the response code is 202 will verify that we look for the log file only - // if we expect it to be there - if resp.StatusCode == 202 { - injectClientCertError := consumeInjectClientCertError(auth.Config.InjectCertLogPath) - if injectClientCertError != "" { - log.Error(log.CAKC055, injectClientCertError) - } - } - span.RecordErrorAndSetStatus(err) - span.End() - return err - } - span.End() - - _, span = tracer.Start(ctx, "Load cert file") - // load client cert - certPEMBlock, err := ioutil.ReadFile(auth.Config.ClientCertPath) - if err != nil { - span.RecordErrorAndSetStatus(err) - span.End() - - if os.IsNotExist(err) { - return log.RecordedError(log.CAKC011, auth.Config.ClientCertPath) - } - - return log.RecordedError(log.CAKC012, err) - } - log.Debug(log.CAKC049, auth.Config.ClientCertPath) - - certDERBlock, certPEMBlock := pem.Decode(certPEMBlock) - cert, err := x509.ParseCertificate(certDERBlock.Bytes) - if err != nil { - span.RecordErrorAndSetStatus(err) - span.End() - - return log.RecordedError(log.CAKC013, auth.Config.ClientCertPath, err) - } - - auth.PublicCert = cert - span.End() - - // clean up the client cert so it's only available in memory - os.Remove(auth.Config.ClientCertPath) - log.Debug(log.CAKC050) - - return nil -} - -// IsLoggedIn returns true if we are logged in (have a cert) -func (auth *Authenticator) IsLoggedIn() bool { - return auth.PublicCert != nil -} - -// isCertExpired returns true if certificate is expired or close to expiring -func (auth *Authenticator) isCertExpired() bool { - certExpiresOn := auth.PublicCert.NotAfter.UTC() - currentDate := time.Now().UTC() - - log.Debug(log.CAKC042, certExpiresOn) - log.Debug(log.CAKC043, currentDate) - log.Debug(log.CAKC044, bufferTime) - - return currentDate.Add(bufferTime).After(certExpiresOn) -} - -// loginIfNeeded checks if we need to send a login request to Conjur and sends -// one if needed -func (auth *Authenticator) loginIfNeeded(ctx context.Context, tracer trace.Tracer) error { - if !auth.IsLoggedIn() { - log.Debug(log.CAKC039) - - if err := auth.login(ctx, tracer); err != nil { - return log.RecordedError(log.CAKC015) - } - - log.Debug(log.CAKC036) - } - - if auth.isCertExpired() { - log.Debug(log.CAKC038) - - if err := auth.login(ctx, tracer); err != nil { - return err - } - - log.Debug(log.CAKC037) - } - - return nil -} - -// sendAuthenticationRequest reads the cert from memory and uses it to send -// an authentication request to the Conjur server. It also validates the response -// code before returning its body -func (auth *Authenticator) sendAuthenticationRequest(ctx context.Context, tracer trace.Tracer) ([]byte, error) { - _, span := tracer.Start(ctx, "Send authentication request") - defer span.End() - - privDer := x509.MarshalPKCS1PrivateKey(auth.privateKey) - keyPEMBlock := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privDer}) - - certPEMBlock := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: auth.PublicCert.Raw}) - - client, err := newHTTPSClient(auth.Config.SSLCertificate, certPEMBlock, keyPEMBlock) - if err != nil { - span.RecordErrorAndSetStatus(err) - return nil, err - } - - req, err := AuthenticateRequest( - auth.Config.URL, - auth.Config.ConjurVersion, - auth.Config.Account, - auth.Config.Username.FullUsername, - ) - if err != nil { - span.RecordErrorAndSetStatus(err) - return nil, err - } - - resp, err := client.Do(req) - if err != nil { - span.RecordErrorAndSetStatus(err) - return nil, log.RecordedError(log.CAKC027, err) - } - - err = utils.ValidateResponse(resp) - if err != nil { - span.RecordErrorAndSetStatus(err) - return nil, err - } - - return utils.ReadResponseBody(resp) -} - -// parseAuthenticationResponse takes the response from the Authenticate -// request, decrypts if needed, and returns it -func (auth *Authenticator) parseAuthenticationResponse(ctx context.Context, tracer trace.Tracer, response []byte) ([]byte, error) { - _, span := tracer.Start(ctx, "Parse authentication response") - defer span.End() - - var content []byte - var err error - - // Token is only encrypted in Conjur v4 - if auth.Config.ConjurVersion == "4" { - content, err = decodeFromPEM(response, auth.PublicCert, auth.privateKey) - if err != nil { - span.RecordErrorAndSetStatus(err) - return nil, log.RecordedError(log.CAKC020) - } - } else if auth.Config.ConjurVersion == "5" { - content = response - } - - return content, nil -} - -// generateSANURI returns the formatted uri(SPIFFEE format for now) for the certificate. -func generateSANURI(namespace, podname string) (string, error) { - if namespace == "" || podname == "" { - return "", log.RecordedError(log.CAKC008, namespace, podname) - } - return fmt.Sprintf("spiffe://cluster.local/namespace/%s/podname/%s", namespace, podname), nil -} - -func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL) ([]byte, error) { - var rawValues []asn1.RawValue - for _, name := range dnsNames { - rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeDNS, Class: asn1.ClassContextSpecific, Bytes: []byte(name)}) - } - for _, email := range emailAddresses { - rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeEmail, Class: asn1.ClassContextSpecific, Bytes: []byte(email)}) - } - for _, rawIP := range ipAddresses { - // If possible, we always want to encode IPv4 addresses in 4 bytes. - ip := rawIP.To4() - if ip == nil { - ip = rawIP - } - rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeIP, Class: asn1.ClassContextSpecific, Bytes: ip}) - } - for _, uri := range uris { - rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeURI, Class: asn1.ClassContextSpecific, Bytes: []byte(uri.String())}) - } - return asn1.Marshal(rawValues) -} - -func decodeFromPEM(PEMBlock []byte, publicCert *x509.Certificate, privateKey crypto.PrivateKey) ([]byte, error) { - var decodedPEM []byte - - tokenDerBlock, _ := pem.Decode(PEMBlock) - p7, err := pkcs7.Parse(tokenDerBlock.Bytes) - if err != nil { - return nil, log.RecordedError(log.CAKC026, err) - } - - decodedPEM, err = p7.Decrypt(publicCert, privateKey) - if err != nil { - return nil, log.RecordedError(log.CAKC025, err) - } - - return decodedPEM, nil -} - -func consumeInjectClientCertError(path string) string { - // The log file will not exist in old Conjur versions - err := utils.VerifyFileExists(path) - if err != nil { - log.Warn(log.CAKC056, path) - return "" - } - - content, err := ioutil.ReadFile(path) - if err != nil { - log.Error(log.CAKC053, path) - return "" - } - - log.Debug(log.CAKC057, path) - err = os.Remove(path) - if err != nil { - log.Error(log.CAKC054, path) - } - - return string(content) +type Authenticator interface { + Authenticate() error + AuthenticateWithContext(ctx context.Context) error } diff --git a/pkg/authenticator/authenticator_factory.go b/pkg/authenticator/authenticator_factory.go new file mode 100644 index 00000000..e5ea2b26 --- /dev/null +++ b/pkg/authenticator/authenticator_factory.go @@ -0,0 +1,33 @@ +package authenticator + +import ( + "fmt" + "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token" + "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token/file" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" + k8sAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" +) + +// NewAuthenticator creates an instance of the Authenticator interface based on configured authenticator type. +func NewAuthenticator(conf config.Configuration) (Authenticator, error) { + accessToken, error := file.NewAccessToken(conf.GetTokenFilePath()) + if error != nil { + return nil, error + } + return getAuthenticator(conf, accessToken) +} + +// NewAuthenticatorWithAccessToken creates an instance of the Authenticator interface based on configured authenticator type +// and access token +func NewAuthenticatorWithAccessToken(conf config.Configuration, token access_token.AccessToken) (Authenticator, error) { + return getAuthenticator(conf, token) +} + +func getAuthenticator(conf config.Configuration, token access_token.AccessToken) (Authenticator, error) { + if conf.GetAuthenticationType() == k8sAuthenticator.AuthnType { + k8sCfg := (conf).(*k8sAuthenticator.Config) + return k8sAuthenticator.NewWithAccessToken(*k8sCfg, token) + } + return nil, fmt.Errorf(log.CAKC064) +} diff --git a/pkg/authenticator/client.go b/pkg/authenticator/common/client.go similarity index 86% rename from pkg/authenticator/client.go rename to pkg/authenticator/common/client.go index 0c66103a..b11ca84b 100644 --- a/pkg/authenticator/client.go +++ b/pkg/authenticator/common/client.go @@ -1,4 +1,4 @@ -package authenticator +package common import ( "crypto/tls" @@ -9,7 +9,8 @@ import ( "github.com/cyberark/conjur-authn-k8s-client/pkg/log" ) -func newHTTPSClient(CACert []byte, certPEMBlock, keyPEMBlock []byte) (*http.Client, error) { +// NewHTTPSClient Returns https client to communicate with Conjur +func NewHTTPSClient(CACert []byte, certPEMBlock, keyPEMBlock []byte) (*http.Client, error) { caCertPool := x509.NewCertPool() ok := caCertPool.AppendCertsFromPEM(CACert) if !ok { diff --git a/pkg/authenticator/common/common_config.go b/pkg/authenticator/common/common_config.go new file mode 100644 index 00000000..b3b920fb --- /dev/null +++ b/pkg/authenticator/common/common_config.go @@ -0,0 +1,58 @@ +package common + +import ( + "fmt" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" + "strconv" + "time" +) + +// Config defines the configuration parameters common for both authentications +type Config struct { + Account string + ClientCertPath string + ClientCertRetryCountLimit int + ContainerMode string + SSLCertificate []byte + TokenFilePath string + TokenRefreshTimeout time.Duration + URL string + Username *Username +} + +// LoadConfig is a constructor for common Config object +func (config *Config) LoadConfig(settings map[string]string) { + for key, value := range settings { + switch key { + case "CONJUR_ACCOUNT": + config.Account = value + case "CONJUR_AUTHN_LOGIN": + username, _ := NewUsername(value) + config.Username = username + case "CONJUR_AUTHN_URL": + config.URL = value + case "CONJUR_SSL_CERTIFICATE": + config.SSLCertificate = []byte(value) + case "CONTAINER_MODE": + config.ContainerMode = value + case "CONJUR_AUTHN_TOKEN_FILE": + config.TokenFilePath = value + case "CONJUR_CLIENT_CERT_PATH": + config.ClientCertPath = value + case "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": + limit, _ := strconv.Atoi(value) + config.ClientCertRetryCountLimit = limit + case "CONJUR_TOKEN_TIMEOUT": + timeout, _ := durationFromString(key, value) + config.TokenRefreshTimeout = timeout + } + } +} + +func durationFromString(key, value string) (time.Duration, error) { + duration, err := time.ParseDuration(value) + if err != nil { + return 0, fmt.Errorf(log.CAKC060, key, value) + } + return duration, nil +} diff --git a/pkg/authenticator/config/username.go b/pkg/authenticator/common/username.go similarity index 99% rename from pkg/authenticator/config/username.go rename to pkg/authenticator/common/username.go index 7b26927e..e6541461 100644 --- a/pkg/authenticator/config/username.go +++ b/pkg/authenticator/common/username.go @@ -1,4 +1,4 @@ -package config +package common import ( "strings" diff --git a/pkg/authenticator/config/username_test.go b/pkg/authenticator/common/username_test.go similarity index 89% rename from pkg/authenticator/config/username_test.go rename to pkg/authenticator/common/username_test.go index f1abba0e..f990bbdd 100644 --- a/pkg/authenticator/config/username_test.go +++ b/pkg/authenticator/common/username_test.go @@ -1,4 +1,4 @@ -package config +package common import ( "fmt" @@ -19,7 +19,7 @@ func TestNewUsername(t *testing.T) { // ASSERT assert.Equal(t, "host.path.to.policy", usernameStruct.Prefix) - assert.Equal(t, "namespace.resource_type.resource_id", usernameStruct.Suffix) + assert.Equal(t, "namespace.resource_type.resource_id", usernameStruct.Suffix) }) t.Run("shorter than 4 parts", func(t *testing.T) { @@ -31,10 +31,9 @@ func TestNewUsername(t *testing.T) { // ASSERT assert.Equal(t, "host.policy", usernameStruct.Prefix) - assert.Equal(t, "host_id", usernameStruct.Suffix) + assert.Equal(t, "host_id", usernameStruct.Suffix) }) - t.Run("missing host/ prefix", func(t *testing.T) { // SETUP & EXERCISE _, err := NewUsername("namespace/resource_type/resource_id") diff --git a/pkg/authenticator/common/validations.go b/pkg/authenticator/common/validations.go new file mode 100644 index 00000000..5ba7beff --- /dev/null +++ b/pkg/authenticator/common/validations.go @@ -0,0 +1,72 @@ +package common + +import ( + "errors" + "fmt" + "strconv" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" +) + +// ReadFileFunc defines the interface for reading an SSL Certificate from the env +type ReadFileFunc func(filename string) ([]byte, error) + +func validTimeout(key, timeoutStr string) error { + _, err := durationFromString(key, timeoutStr) + return err +} + +func validInt(key, value string) error { + _, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf(log.CAKC060, key, value) + } + return nil +} + +func validUsername(key, value string) error { + _, err := NewUsername(value) + return err +} + +func validConjurVersion(key, version string) error { + // Only versions '4' & '5' are allowed, with '5' being used as the default + switch version { + case "4": + break + case "5": + break + default: + return fmt.Errorf(log.CAKC060, key, version) + } + + return nil +} + +func ValidateSetting(key string, value string) error { + switch key { + case "CONJUR_AUTHN_LOGIN": + return validUsername(key, value) + case "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": + return validInt(key, value) + case "CONJUR_TOKEN_TIMEOUT": + return validTimeout(key, value) + case "CONJUR_VERSION": + return validConjurVersion(key, value) + default: + return nil + } +} + +func ReadSSLCert(settings map[string]string, readFile ReadFileFunc) ([]byte, error) { + SSLCert := settings["CONJUR_SSL_CERTIFICATE"] + SSLCertPath := settings["CONJUR_CERT_FILE"] + if SSLCert == "" && SSLCertPath == "" { + return nil, errors.New(log.CAKC007) + } + + if SSLCert != "" { + return []byte(SSLCert), nil + } + return readFile(SSLCertPath) +} diff --git a/pkg/authenticator/config/config.go b/pkg/authenticator/config/config.go deleted file mode 100644 index 01f4e2a7..00000000 --- a/pkg/authenticator/config/config.go +++ /dev/null @@ -1,293 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "io/ioutil" - "os" - "strconv" - "time" - - "github.com/cyberark/conjur-authn-k8s-client/pkg/log" -) - -// Config defines the configuration parameters -// for the authentication requests -type Config struct { - Account string - ClientCertPath string - ClientCertRetryCountLimit int - ContainerMode string - ConjurVersion string - InjectCertLogPath string - PodName string - PodNamespace string - SSLCertificate []byte - TokenFilePath string - TokenRefreshTimeout time.Duration - URL string - Username *Username -} - -// AuthnSettings represents a group of authenticator client configuration settings. -type AuthnSettings map[string]string - -// Default settings (this comment added to satisfy linter) -const ( - DefaultClientCertPath = "/etc/conjur/ssl/client.pem" - DefaultInjectCertLogPath = "/tmp/conjur_copy_text_output.log" - DefaultTokenFilePath = "/run/conjur/access-token" - - DefaultConjurVersion = "5" - - // DefaultTokenRefreshTimeout is the default time the system waits to reauthenticate on error - DefaultTokenRefreshTimeout = "6m0s" - - // DefaultClientCertRetryCountLimit is the amount of times we wait after successful - // login for the client certificate file to exist, where each time we wait for a second. - DefaultClientCertRetryCountLimit = "10" -) - -var requiredEnvVariables = []string{ - "CONJUR_AUTHN_URL", - "CONJUR_ACCOUNT", - "CONJUR_AUTHN_LOGIN", - "MY_POD_NAMESPACE", - "MY_POD_NAME", -} - -var envVariables = []string{ - "CONJUR_ACCOUNT", - "CONJUR_AUTHN_LOGIN", - "CONJUR_AUTHN_TOKEN_FILE", - "CONJUR_AUTHN_URL", - "CONJUR_CERT_FILE", - "CONJUR_CLIENT_CERT_PATH", - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT", - "CONJUR_SSL_CERTIFICATE", - "CONJUR_TOKEN_TIMEOUT", - "CONJUR_VERSION", - "CONTAINER_MODE", - "DEBUG", - "MY_POD_NAME", - "MY_POD_NAMESPACE", -} - -var defaultValues = map[string]string{ - "CONJUR_CLIENT_CERT_PATH": DefaultClientCertPath, - "CONJUR_AUTHN_TOKEN_FILE": DefaultTokenFilePath, - "CONJUR_VERSION": DefaultConjurVersion, - "CONJUR_TOKEN_TIMEOUT": DefaultTokenRefreshTimeout, - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": DefaultClientCertRetryCountLimit, -} - -func defaultEnv(key string) string { - return defaultValues[key] -} - -// ReadFileFunc defines the interface for reading an SSL Certificate from the env -type ReadFileFunc func(filename string) ([]byte, error) - -// NewFromEnv returns a config FromEnv using the standard file reader for reading certs -func NewFromEnv() (*Config, error) { - return FromEnv(ioutil.ReadFile) -} - -// FromEnv returns a new authenticator configuration object -func FromEnv(readFileFunc ReadFileFunc) (*Config, error) { - envSettings := GatherSettings(os.Getenv) - - errLogs := envSettings.Validate(readFileFunc) - if len(errLogs) > 0 { - logErrors(errLogs) - return nil, errors.New(log.CAKC061) - } - - return envSettings.NewConfig(), nil -} - -// GatherSettings retrieves authenticator client configuration settings from a slice -// of arbitrary `func(key string) string` functions. Values received from 'Getter' functions -// are prioritized in the order that the functions are provided. -func GatherSettings(getters ...func(key string) string) AuthnSettings { - getters = append(getters, defaultEnv) - - getEnv := func(key string) string { - var val string - for _, getter := range getters { - val = getter(key) - if len(val) > 0 { - return val - } - } - return "" - } - - settings := make(AuthnSettings) - - for _, key := range envVariables { - value := getEnv(key) - settings[key] = value - } - - return settings -} - -// Validate confirms that the given AuthnSettings yield a valid authenticator -// client configuration. Returns a list of Error logs. -func (settings AuthnSettings) Validate(readFileFunc ReadFileFunc) []error { - errorLogs := []error{} - - // ensure required values exist - for _, key := range requiredEnvVariables { - if settings[key] == "" { - errorLogs = append(errorLogs, fmt.Errorf(log.CAKC062, key)) - } - } - - // ensure provided values are of the correct type - for _, key := range envVariables { - err := validateSetting(key, settings[key]) - if err != nil { - errorLogs = append(errorLogs, err) - } - } - - // ensure that the certificate settings are valid - cert, err := readSSLCert(settings, readFileFunc) - if err != nil { - errorLogs = append(errorLogs, err) - } else { - if settings["CONJUR_SSL_CERTIFICATE"] == "" { - settings["CONJUR_SSL_CERTIFICATE"] = string(cert) - } - } - - return errorLogs -} - -// NewConfig provides a new authenticator configuration from an AuthnSettings map. -func (settings AuthnSettings) NewConfig() *Config { - configureLogLevel(settings["DEBUG"]) - - config := &Config{} - config.InjectCertLogPath = DefaultInjectCertLogPath - - for key, value := range settings { - switch key { - case "CONJUR_ACCOUNT": - config.Account = value - case "CONJUR_AUTHN_LOGIN": - username, _ := NewUsername(value) - config.Username = username - case "CONJUR_AUTHN_URL": - config.URL = value - case "CONJUR_SSL_CERTIFICATE": - config.SSLCertificate = []byte(value) - case "CONTAINER_MODE": - config.ContainerMode = value - case "MY_POD_NAME": - config.PodName = value - case "MY_POD_NAMESPACE": - config.PodNamespace = value - case "CONJUR_AUTHN_TOKEN_FILE": - config.TokenFilePath = value - case "CONJUR_CLIENT_CERT_PATH": - config.ClientCertPath = value - case "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": - limit, _ := strconv.Atoi(value) - config.ClientCertRetryCountLimit = limit - case "CONJUR_TOKEN_TIMEOUT": - timeout, _ := durationFromString(key, value) - config.TokenRefreshTimeout = timeout - case "CONJUR_VERSION": - config.ConjurVersion = value - } - } - - return config -} - -func configureLogLevel(level string) { - validVal := "true" - if level == validVal { - log.EnableDebugMode() - } else if level != "" { - // In case "DEBUG" is configured with incorrect value - log.Warn(log.CAKC034, level, validVal) - } -} - -func logErrors(errLogs []error) { - for _, err := range errLogs { - log.Error(err.Error()) - } -} - -func validTimeout(key, timeoutStr string) error { - _, err := durationFromString(key, timeoutStr) - return err -} - -func durationFromString(key, value string) (time.Duration, error) { - duration, err := time.ParseDuration(value) - if err != nil { - return 0, fmt.Errorf(log.CAKC060, key, value) - } - return duration, nil -} - -func validInt(key, value string) error { - _, err := strconv.Atoi(value) - if err != nil { - return fmt.Errorf(log.CAKC060, key, value) - } - return nil -} - -func validUsername(key, value string) error { - _, err := NewUsername(value) - return err -} - -func validConjurVersion(key, version string) error { - // Only versions '4' & '5' are allowed, with '5' being used as the default - switch version { - case "4": - break - case "5": - break - default: - return fmt.Errorf(log.CAKC060, key, version) - } - - return nil -} - -func validateSetting(key string, value string) error { - switch key { - case "CONJUR_AUTHN_LOGIN": - return validUsername(key, value) - case "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": - return validInt(key, value) - case "CONJUR_TOKEN_TIMEOUT": - return validTimeout(key, value) - case "CONJUR_VERSION": - return validConjurVersion(key, value) - default: - return nil - } -} - -func readSSLCert(settings map[string]string, readFile ReadFileFunc) ([]byte, error) { - SSLCert := settings["CONJUR_SSL_CERTIFICATE"] - SSLCertPath := settings["CONJUR_CERT_FILE"] - if SSLCert == "" && SSLCertPath == "" { - return nil, errors.New(log.CAKC007) - } - - if SSLCert != "" { - return []byte(SSLCert), nil - } - return readFile(SSLCertPath) -} diff --git a/pkg/authenticator/config/config_test.go b/pkg/authenticator/config/config_test.go deleted file mode 100644 index 0de520db..00000000 --- a/pkg/authenticator/config/config_test.go +++ /dev/null @@ -1,501 +0,0 @@ -package config - -import ( - "bytes" - "errors" - "fmt" - "log" - "os" - "testing" - - logger "github.com/cyberark/conjur-authn-k8s-client/pkg/log" - "github.com/stretchr/testify/assert" -) - -var environmentValues = map[string]string{ - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host", - "CONJUR_CERT_FILE": "testSSLCertFile.txt", - "CONJUR_SSL_CERTIFICATE": "testSSLCert", - "CONJUR_VERSION": "", - "MY_POD_NAMESPACE": "testNameSpace", - "MY_POD_NAME": "testPodName", -} - -var annotationValues = map[string]string{ - "conjur.org/authn-identity": "host/anotherHost", - "conjur.org/debug-logging": "true", - "conjur.org/container-mode": "init", -} - -var envToAnnot = map[string]string{ - "CONJUR_AUTHN_LOGIN": "conjur.org/authn-identity", - "DEBUG": "conjur.org/debug-logging", - "CONTAINER_MODE": "conjur.org/container-mode", -} - -func assertGoodConfig(expected *Config) func(*testing.T, *Config) { - return func(t *testing.T, result *Config) { - assert.Equal(t, expected, result) - } -} - -type errorAssertFunc func(*testing.T, []error) - -func assertEmptyErrorList() errorAssertFunc { - return func(t *testing.T, errorList []error) { - assert.Empty(t, errorList) - } -} - -func assertErrorInList(err error) errorAssertFunc { - return func(t *testing.T, errorList []error) { - assert.Contains(t, errorList, err) - } -} - -func assertErrorNotInList(err error) errorAssertFunc { - return func(t *testing.T, errorList []error) { - assert.NotContains(t, errorList, err) - } -} - -func setEnv(env map[string]string) { - for key, value := range env { - os.Setenv(key, value) - } -} - -func unsetEnv(env map[string]string) { - for key := range env { - os.Setenv(key, "") - } -} - -func TestGatherSettings(t *testing.T) { - TestCases := []struct { - description string - annotFunc func(string) string - expected AuthnSettings - }{ - { - description: "functions are ordered by priority: first function overrides second, which overrides third", - annotFunc: fromAnnotations, - expected: AuthnSettings{ - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host/anotherHost", // provided by annotation - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_CERT_FILE": "testSSLCertFile.txt", - "CONJUR_SSL_CERTIFICATE": "testSSLCert", - "CONTAINER_MODE": "init", // provided by annotation - "DEBUG": "true", // provided by annotation - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - "CONJUR_AUTHN_TOKEN_FILE": DefaultTokenFilePath, - "CONJUR_CLIENT_CERT_PATH": DefaultClientCertPath, - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": DefaultClientCertRetryCountLimit, - "CONJUR_TOKEN_TIMEOUT": DefaultTokenRefreshTimeout, - "CONJUR_VERSION": DefaultConjurVersion, - }, - }, - { - description: "if the first getter function returns empty strings, fallback to the next functions, and eventually an empty string", - annotFunc: emptyAnnotations, - expected: AuthnSettings{ - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host", - "CONJUR_CERT_FILE": "testSSLCertFile.txt", - "CONJUR_SSL_CERTIFICATE": "testSSLCert", - "MY_POD_NAMESPACE": "testNameSpace", - "MY_POD_NAME": "testPodName", - "DEBUG": "", - "CONTAINER_MODE": "", - "CONJUR_CLIENT_CERT_PATH": DefaultClientCertPath, - "CONJUR_AUTHN_TOKEN_FILE": DefaultTokenFilePath, - "CONJUR_VERSION": DefaultConjurVersion, - "CONJUR_TOKEN_TIMEOUT": DefaultTokenRefreshTimeout, - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": DefaultClientCertRetryCountLimit, - }, - }, - } - - for _, tc := range TestCases { - t.Run(tc.description, func(t *testing.T) { - resultMap := GatherSettings(tc.annotFunc, fromEnv) - assert.Equal(t, tc.expected, resultMap) - }) - } -} - -func TestValidate(t *testing.T) { - TestCases := []struct { - description string - settings AuthnSettings - assert errorAssertFunc - }{ - { - description: "happy path", - settings: AuthnSettings{ - // required variables - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - // correct value types - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", - "CONJUR_TOKEN_TIMEOUT": "6m0s", - "CONTAINER_MODE": "init", - // certificate provided - "CONJUR_SSL_CERTIFICATE": "samplecertificate", - // valid version - "CONJUR_VERSION": "5", - }, - assert: assertEmptyErrorList(), - }, - { - description: "error raised for missing required setting", - settings: AuthnSettings{}, - assert: assertErrorInList(fmt.Errorf(logger.CAKC062, "CONJUR_AUTHN_URL")), - }, - { - description: "error raised for invalid username", - settings: AuthnSettings{ - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "bad-username", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - }, - assert: assertErrorInList(fmt.Errorf(logger.CAKC032, "bad-username")), - }, - { - description: "error raised for invalid retry count limit", - settings: AuthnSettings{ - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "seven", - }, - assert: assertErrorInList(fmt.Errorf(logger.CAKC060, "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT", "seven")), - }, - { - description: "error raised for invalid timeout", - settings: AuthnSettings{ - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", - "CONJUR_TOKEN_TIMEOUT": "seventeen", - }, - assert: assertErrorInList(fmt.Errorf(logger.CAKC060, "CONJUR_TOKEN_TIMEOUT", "seventeen")), - }, - { - description: "error raised for invalid certificate", - settings: AuthnSettings{ - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", - "CONJUR_TOKEN_TIMEOUT": "6m0s", - "CONTAINER_MODE": "init", - "CONJUR_SSL_CERTIFICATE": "", - "CONJUR_CERT_FILE": "", - }, - assert: assertErrorInList(errors.New(logger.CAKC007)), - }, - } - - for _, tc := range TestCases { - t.Run(tc.description, func(t *testing.T) { - // SETUP & EXERCISE - errLogs := tc.settings.Validate(successfulMockReadFile) - - // ASSERT - tc.assert(t, errLogs) - }) - } -} - -func TestNewConfig(t *testing.T) { - TestCases := []struct { - description string - settings AuthnSettings - expected *Config - assert func(*testing.T, *Config) - }{ - { - description: "happy path", - settings: AuthnSettings{ - // required variables - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host/test-user", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - // correct value types - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", - "CONJUR_TOKEN_TIMEOUT": "6m0s", - "CONTAINER_MODE": "init", - // certificate provided - "CONJUR_SSL_CERTIFICATE": "samplecertificate", - // defaults provided - "CONJUR_AUTHN_TOKEN_FILE": DefaultTokenFilePath, - "CONJUR_CLIENT_CERT_PATH": DefaultClientCertPath, - "CONJUR_VERSION": DefaultConjurVersion, - }, - assert: assertGoodConfig(&Config{ - Account: "testAccount", - ClientCertPath: DefaultClientCertPath, - ClientCertRetryCountLimit: 7, - ContainerMode: "init", - ConjurVersion: "5", - InjectCertLogPath: DefaultInjectCertLogPath, - PodName: "testPodName", - PodNamespace: "testNameSpace", - SSLCertificate: []byte("samplecertificate"), - TokenFilePath: DefaultTokenFilePath, - TokenRefreshTimeout: 360000000000, - URL: "filepath", - Username: &Username{ - FullUsername: "host/test-user", - Prefix: "host", - Suffix: "test-user", - }, - }), - }, - } - - for _, tc := range TestCases { - t.Run(tc.description, func(t *testing.T) { - config := tc.settings.NewConfig() - tc.assert(t, config) - }) - } -} - -func TestFromEnv(t *testing.T) { - TestCases := []struct { - description string - env map[string]string - expectErr bool - }{ - { - description: "happy path", - env: map[string]string{ - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host/test-user", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - "CONJUR_SSL_CERTIFICATE": "samplecert", - }, - expectErr: false, - }, - { - description: "bad settings return nil Config and error message", - env: map[string]string{}, - expectErr: true, - }, - } - - for _, tc := range TestCases { - t.Run(tc.description, func(t *testing.T) { - // SETUP & EXERCISE - setEnv(tc.env) - config, err := FromEnv(successfulMockReadFile) - - // ASSERT - if tc.expectErr { - assert.Nil(t, config) - assert.NotNil(t, err) - } else { - assert.NotNil(t, config) - assert.Nil(t, err) - } - unsetEnv(tc.env) - }) - } -} - -func TestConjurVersion(t *testing.T) { - TestCases := []struct { - description string - version string - expVersion string - assert errorAssertFunc - }{ - { - description: "Succeeds if version is 4", - version: "4", - expVersion: "4", - assert: assertErrorNotInList(fmt.Errorf(logger.CAKC060, "CONJUR_VERSION", "4")), - }, - { - description: "Succeeds if version is 5", - version: "5", - expVersion: "5", - assert: assertErrorNotInList(fmt.Errorf(logger.CAKC060, "CONJUR_VERSION", "5")), - }, - { - description: "Sets the default version for an empty value", - version: "", - expVersion: DefaultConjurVersion, - assert: assertErrorNotInList(fmt.Errorf(logger.CAKC060, "CONJUR_VERSION", DefaultConjurVersion)), - }, - { - description: "Returns error if version is invalid", - version: "3", - expVersion: "", - assert: assertErrorInList(fmt.Errorf(logger.CAKC060, "CONJUR_VERSION", "3")), - }, - } - - for _, tc := range TestCases { - provideVersion := func(key string) string { - if key == "CONJUR_VERSION" { - return tc.version - } - return "" - } - - t.Run(tc.description, func(t *testing.T) { - // SETUP & EXERCISE - settings := GatherSettings(provideVersion) - errLogs := settings.Validate(successfulMockReadFile) - - // ASSERT - tc.assert(t, errLogs) - if tc.expVersion != "" { - assert.Equal(t, tc.expVersion, settings["CONJUR_VERSION"]) - } - }) - } -} - -func TestDebugLogging(t *testing.T) { - TestCases := []struct { - description string - debugValue string - settings AuthnSettings - }{ - { - description: "debug logs are enabled", - debugValue: "true", - settings: AuthnSettings{ - // required variables - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host/test-user", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - // correct value types - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", - "CONJUR_TOKEN_TIMEOUT": "6m0s", - "CONTAINER_MODE": "init", - // certificate provided - "CONJUR_SSL_CERTIFICATE": "samplecertificate", - // defaults provided - "CONJUR_AUTHN_TOKEN_FILE": DefaultTokenFilePath, - "CONJUR_CLIENT_CERT_PATH": DefaultClientCertPath, - "CONJUR_VERSION": DefaultConjurVersion, - // debug setting - "DEBUG": "true", - }, - }, - { - description: "debug logs are disabled", - debugValue: "", - settings: AuthnSettings{ - // required variables - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host/test-user", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - // correct value types - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", - "CONJUR_TOKEN_TIMEOUT": "6m0s", - "CONTAINER_MODE": "init", - // certificate provided - "CONJUR_SSL_CERTIFICATE": "samplecertificate", - // defaults provided - "CONJUR_AUTHN_TOKEN_FILE": DefaultTokenFilePath, - "CONJUR_CLIENT_CERT_PATH": DefaultClientCertPath, - "CONJUR_VERSION": DefaultConjurVersion, - }, - }, - { - description: "debug logs are given an incorrect value", - debugValue: "garbage", - settings: AuthnSettings{ - // required variables - "CONJUR_AUTHN_URL": "filepath", - "CONJUR_ACCOUNT": "testAccount", - "CONJUR_AUTHN_LOGIN": "host/test-user", - "MY_POD_NAME": "testPodName", - "MY_POD_NAMESPACE": "testNameSpace", - // correct value types - "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", - "CONJUR_TOKEN_TIMEOUT": "6m0s", - "CONTAINER_MODE": "init", - // certificate provided - "CONJUR_SSL_CERTIFICATE": "samplecertificate", - // defaults provided - "CONJUR_AUTHN_TOKEN_FILE": DefaultTokenFilePath, - "CONJUR_CLIENT_CERT_PATH": DefaultClientCertPath, - "CONJUR_VERSION": DefaultConjurVersion, - // debug setting - "DEBUG": "garbage", - }, - }, - } - - for _, tc := range TestCases { - // SETUP - var logBuffer bytes.Buffer - logger.InfoLogger = log.New(&logBuffer, "", 0) - - // EXERCISE - config := tc.settings.NewConfig() - assert.NotNil(t, config) - - // ASSERT - logMessages := logBuffer.String() - if tc.debugValue == "true" { - assert.Contains(t, logMessages, "CAKC052") - assert.NotContains(t, logMessages, "CAKC034") - } else if tc.debugValue == "" { - assert.NotContains(t, logMessages, "CAKC052") - assert.NotContains(t, logMessages, "CAKC034") - } else { - assert.NotContains(t, logMessages, "CAKC052") - assert.Contains(t, logMessages, "CAKC034") - } - } -} - -func successfulMockReadFile(filename string) ([]byte, error) { - return []byte{}, nil -} - -func fromEnv(key string) string { - return environmentValues[key] -} - -func fromAnnotations(key string) string { - annot := envToAnnot[key] - return annotationValues[annot] -} - -func emptyAnnotations(key string) string { - return "" -} diff --git a/pkg/authenticator/config/configuration.go b/pkg/authenticator/config/configuration.go new file mode 100644 index 00000000..cc160ff3 --- /dev/null +++ b/pkg/authenticator/config/configuration.go @@ -0,0 +1,17 @@ +package config + +import ( + "time" +) + +// Configuration defines interface for Configuration of an authentication flow +type Configuration interface { + LoadConfig(settings map[string]string) + GetAuthenticationType() string + GetEnvVariables() []string + GetRequiredVariables() []string + GetDefaultValues() map[string]string + GetContainerMode() string + GetTokenFilePath() string + GetTokenTimeout() time.Duration +} diff --git a/pkg/authenticator/config/configuration_factory.go b/pkg/authenticator/config/configuration_factory.go new file mode 100644 index 00000000..80fd2e08 --- /dev/null +++ b/pkg/authenticator/config/configuration_factory.go @@ -0,0 +1,123 @@ +package config + +import ( + "errors" + "fmt" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/common" + "io/ioutil" + "os" + "strings" + + k8sAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" +) + +const authnURLVarName string = "CONJUR_AUTHN_URL" + +// AuthnSettings represents a group of authenticator client configuration settings. +type AuthnSettings map[string]string + +// NewConfigFromEnv returns a config ConfigFromEnv using the standard file reader for reading certs +func NewConfigFromEnv() (Configuration, error) { + return ConfigFromEnv(ioutil.ReadFile) +} + +// ConfigFromEnv returns a new authenticator configuration object +func ConfigFromEnv(readFileFunc common.ReadFileFunc) (Configuration, error) { + authnUrl := os.Getenv(authnURLVarName) + conf, error := getConfiguration(authnUrl) + if error != nil { + return nil, error + } + envSettings := GatherSettings(conf, os.Getenv) + + errLogs := envSettings.validate(conf, readFileFunc) + if len(errLogs) > 0 { + logErrors(errLogs) + return nil, errors.New(log.CAKC061) + } + + conf.LoadConfig(envSettings) + return conf, nil +} + +func getConfiguration(url string) (Configuration, error) { + if strings.Contains(url, k8sAuthenticator.AuthnType) { + return &k8sAuthenticator.Config{}, nil + } + return nil, fmt.Errorf(log.CAKC063, url) +} + +// GatherSettings retrieves authenticator client configuration settings from a slice +// of arbitrary `func(key string) string` functions. Values received from 'Getter' functions +// are prioritized in the order that the functions are provided. +func GatherSettings(conf Configuration, getters ...func(key string) string) AuthnSettings { + defaultVariables := conf.GetDefaultValues() + + getDefault := func(key string) string { + return defaultVariables[key] + } + + getters = append(getters, getDefault) + settings := make(AuthnSettings) + getEnv := getConfigVariable(getters...) + + for _, key := range conf.GetEnvVariables() { + value := getEnv(key) + settings[key] = value + } + + return settings +} + +// Validate confirms that the given AuthnSettings yield a valid authenticator +// client configuration. Returns a list of Error logs. +func (settings AuthnSettings) validate(conf Configuration, readFileFunc common.ReadFileFunc) []error { + errorLogs := []error{} + + // ensure required values exist + for _, key := range conf.GetRequiredVariables() { + if settings[key] == "" { + errorLogs = append(errorLogs, fmt.Errorf(log.CAKC062, key)) + } + } + + // ensure provided values are of the correct type + for _, key := range conf.GetEnvVariables() { + err := common.ValidateSetting(key, settings[key]) + if err != nil { + errorLogs = append(errorLogs, err) + } + } + + // ensure that the certificate settings are valid + cert, err := common.ReadSSLCert(settings, readFileFunc) + if err != nil { + errorLogs = append(errorLogs, err) + } else { + if settings["CONJUR_SSL_CERTIFICATE"] == "" { + settings["CONJUR_SSL_CERTIFICATE"] = string(cert) + } + } + + return errorLogs +} + +func logErrors(errLogs []error) { + for _, err := range errLogs { + log.Error(err.Error()) + } +} + +func getConfigVariable(getters ...func(key string) string) func(string) string { + return func(key string) string { + var val string + for _, getter := range getters { + val = getter(key) + if len(val) > 0 { + return val + } + } + return "" + } +} diff --git a/pkg/authenticator/config/validate_test.go b/pkg/authenticator/config/validate_test.go new file mode 100644 index 00000000..ae4ae0c2 --- /dev/null +++ b/pkg/authenticator/config/validate_test.go @@ -0,0 +1,185 @@ +package config + +import ( + "errors" + "fmt" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" + logger "github.com/cyberark/conjur-authn-k8s-client/pkg/log" + "github.com/stretchr/testify/assert" + "testing" +) + +type errorAssertFunc func(*testing.T, []error) + +func TestValidate(t *testing.T) { + TestCases := []struct { + description string + settings AuthnSettings + assert errorAssertFunc + }{ + { + description: "happy path", + settings: AuthnSettings{ + // required variables + "CONJUR_AUTHN_URL": "filepath", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_LOGIN": "host", + "MY_POD_NAME": "testPodName", + "MY_POD_NAMESPACE": "testNameSpace", + // correct value types + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", + "CONJUR_TOKEN_TIMEOUT": "6m0s", + "CONTAINER_MODE": "init", + // certificate provided + "CONJUR_SSL_CERTIFICATE": "samplecertificate", + // valid version + "CONJUR_VERSION": "5", + }, + assert: assertEmptyErrorList(), + }, + { + description: "error raised for missing required setting", + settings: AuthnSettings{}, + assert: assertErrorInList(fmt.Errorf(logger.CAKC062, "CONJUR_AUTHN_URL")), + }, + { + description: "error raised for invalid username", + settings: AuthnSettings{ + "CONJUR_AUTHN_URL": "filepath", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_LOGIN": "bad-username", + "MY_POD_NAME": "testPodName", + "MY_POD_NAMESPACE": "testNameSpace", + }, + assert: assertErrorInList(fmt.Errorf(logger.CAKC032, "bad-username")), + }, + { + description: "error raised for invalid retry count limit", + settings: AuthnSettings{ + "CONJUR_AUTHN_URL": "filepath", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_LOGIN": "host", + "MY_POD_NAME": "testPodName", + "MY_POD_NAMESPACE": "testNameSpace", + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "seven", + }, + assert: assertErrorInList(fmt.Errorf(logger.CAKC060, "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT", "seven")), + }, + { + description: "error raised for invalid timeout", + settings: AuthnSettings{ + "CONJUR_AUTHN_URL": "filepath", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_LOGIN": "host", + "MY_POD_NAME": "testPodName", + "MY_POD_NAMESPACE": "testNameSpace", + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", + "CONJUR_TOKEN_TIMEOUT": "seventeen", + }, + assert: assertErrorInList(fmt.Errorf(logger.CAKC060, "CONJUR_TOKEN_TIMEOUT", "seventeen")), + }, + { + description: "error raised for invalid certificate", + settings: AuthnSettings{ + "CONJUR_AUTHN_URL": "filepath", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_LOGIN": "host", + "MY_POD_NAME": "testPodName", + "MY_POD_NAMESPACE": "testNameSpace", + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": "7", + "CONJUR_TOKEN_TIMEOUT": "6m0s", + "CONTAINER_MODE": "init", + "CONJUR_SSL_CERTIFICATE": "", + "CONJUR_CERT_FILE": "", + }, + assert: assertErrorInList(errors.New(logger.CAKC007)), + }, + } + + for _, tc := range TestCases { + t.Run(tc.description, func(t *testing.T) { + // SETUP & EXERCISE + errLogs := tc.settings.validate(&k8s.Config{}, successfulMockReadFile) + + // ASSERT + tc.assert(t, errLogs) + }) + } +} + +func TestConjurVersion(t *testing.T) { + TestCases := []struct { + description string + version string + expVersion string + assert errorAssertFunc + }{ + { + description: "Succeeds if version is 4", + version: "4", + expVersion: "4", + assert: assertErrorNotInList(fmt.Errorf(logger.CAKC060, "CONJUR_VERSION", "4")), + }, + { + description: "Succeeds if version is 5", + version: "5", + expVersion: "5", + assert: assertErrorNotInList(fmt.Errorf(logger.CAKC060, "CONJUR_VERSION", "5")), + }, + { + description: "Sets the default version for an empty value", + version: "", + expVersion: k8s.DefaultConjurVersion, + assert: assertErrorNotInList(fmt.Errorf(logger.CAKC060, "CONJUR_VERSION", k8s.DefaultConjurVersion)), + }, + { + description: "Returns error if version is invalid", + version: "3", + expVersion: "", + assert: assertErrorInList(fmt.Errorf(logger.CAKC060, "CONJUR_VERSION", "3")), + }, + } + + for _, tc := range TestCases { + provideVersion := func(key string) string { + if key == "CONJUR_VERSION" { + return tc.version + } + return "" + } + + t.Run(tc.description, func(t *testing.T) { + // SETUP & EXERCISE + settings := GatherSettings(&k8s.Config{}, provideVersion) + errLogs := settings.validate(&k8s.Config{}, successfulMockReadFile) + + // ASSERT + tc.assert(t, errLogs) + if tc.expVersion != "" { + assert.Equal(t, tc.expVersion, settings["CONJUR_VERSION"]) + } + }) + } +} + +func assertErrorInList(err error) errorAssertFunc { + return func(t *testing.T, errorList []error) { + assert.Contains(t, errorList, err) + } +} + +func successfulMockReadFile(filename string) ([]byte, error) { + return []byte{}, nil +} + +func assertEmptyErrorList() errorAssertFunc { + return func(t *testing.T, errorList []error) { + assert.Empty(t, errorList) + } +} + +func assertErrorNotInList(err error) errorAssertFunc { + return func(t *testing.T, errorList []error) { + assert.NotContains(t, errorList, err) + } +} diff --git a/pkg/authenticator/k8s/authenticator.go b/pkg/authenticator/k8s/authenticator.go new file mode 100644 index 00000000..14343ca6 --- /dev/null +++ b/pkg/authenticator/k8s/authenticator.go @@ -0,0 +1,419 @@ +package k8s + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "time" + + "github.com/fullsailor/pkcs7" + "go.opentelemetry.io/otel" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/common" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" + "github.com/cyberark/conjur-authn-k8s-client/pkg/utils" + "github.com/cyberark/secrets-provider-for-k8s/pkg/trace" +) + +var oidExtensionSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17} +var bufferTime = 30 * time.Second + +// Authenticator contains the configuration and client +// for the authentication connection to Conjur +type Authenticator struct { + client *http.Client + privateKey *rsa.PrivateKey + AccessToken access_token.AccessToken + Config *Config + PublicCert *x509.Certificate +} + +const ( + nameTypeEmail = 1 + nameTypeDNS = 2 + nameTypeURI = 6 + nameTypeIP = 7 +) + +// NewWithAccessToken creates a new authenticator instance from a given access token +func NewWithAccessToken(config Config, accessToken access_token.AccessToken) (*Authenticator, error) { + signingKey, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, log.RecordedError(log.CAKC030, err) + } + + client, err := common.NewHTTPSClient(config.Common.SSLCertificate, nil, nil) + if err != nil { + return nil, err + } + + return &Authenticator{ + client: client, + privateKey: signingKey, + AccessToken: accessToken, + Config: &config, + }, nil +} + +// Authenticate sends Conjur an authenticate request and writes the response +// to the token file (after decrypting it if needed). It also manages state of +// certificates. +// @deprecated Use AuthenticateWithContext instead +func (auth *Authenticator) Authenticate() error { + return auth.AuthenticateWithContext(context.TODO()) +} + +func (auth *Authenticator) AuthenticateWithContext(ctx context.Context) error { + log.Info(log.CAKC040, auth.Config.Common.Username) + + tr := trace.NewOtelTracer(otel.Tracer("conjur-authn-k8s-client")) + spanCtx, span := tr.Start(ctx, "Authenticate") + defer span.End() + + err := auth.loginIfNeeded(spanCtx, tr) + if err != nil { + span.RecordErrorAndSetStatus(err) + span.End() + return err + } + + authenticationResponse, err := auth.sendAuthenticationRequest(spanCtx, tr) + if err != nil { + span.RecordErrorAndSetStatus(err) + span.End() + return err + } + + parsedResponse, err := auth.parseAuthenticationResponse(spanCtx, tr, authenticationResponse) + if err != nil { + span.RecordErrorAndSetStatus(err) + span.End() + return err + } + + err = auth.AccessToken.Write(parsedResponse) + if err != nil { + return err + } + + log.Info(log.CAKC035) + return nil +} + +// generateCSR prepares the CSR +func (auth *Authenticator) generateCSR(commonName string) ([]byte, error) { + sanURIString, err := generateSANURI(auth.Config.PodNamespace, auth.Config.PodName) + sanURI, err := url.Parse(sanURIString) + if err != nil { + return nil, err + } + + subj := pkix.Name{ + CommonName: commonName, + } + + template := x509.CertificateRequest{ + Subject: subj, + SignatureAlgorithm: x509.SHA256WithRSA, + } + + subjectAltNamesValue, err := marshalSANs(nil, nil, nil, []*url.URL{ + sanURI, + }) + if err != nil { + return nil, err + } + + extSubjectAltName := pkix.Extension{ + Id: oidExtensionSubjectAltName, + Critical: false, + Value: subjectAltNamesValue, + } + template.ExtraExtensions = []pkix.Extension{extSubjectAltName} + + return x509.CreateCertificateRequest(rand.Reader, &template, auth.privateKey) +} + +// login sends Conjur a CSR and verifies that the client cert is +// successfully retrieved +func (auth *Authenticator) login(ctx context.Context, tracer trace.Tracer) error { + + log.Debug(log.CAKC041, auth.Config.Common.Username) + + _, span := tracer.Start(ctx, "Generate CSR") + csrRawBytes, err := auth.generateCSR(auth.Config.Common.Username.Suffix) + + csrBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", Bytes: csrRawBytes, + }) + span.End() + + req, err := LoginRequest(auth.Config.Common.URL, auth.Config.ConjurVersion, csrBytes, auth.Config.Common.Username.Prefix) + if err != nil { + return err + } + + _, span = tracer.Start(ctx, "Send login request") + resp, err := auth.client.Do(req) + if err != nil { + span.RecordErrorAndSetStatus(err) + span.End() + return log.RecordedError(log.CAKC028, err) + } + span.End() + + err = utils.ValidateResponse(resp) + if err != nil { + return log.RecordedError(log.CAKC029, err) + } + + _, span = tracer.Start(ctx, "Wait for cert file") + // Ensure client certificate exists before attempting to read it, with a tolerance + // for small delays + err = utils.WaitForFile( + auth.Config.Common.ClientCertPath, + auth.Config.Common.ClientCertRetryCountLimit, + ) + if err != nil { + // The response code was changed from 200 to 202 in the same Conjur version + // that started writing the cert injection logs to the client. Verifying that + // the response code is 202 will verify that we look for the log file only + // if we expect it to be there + if resp.StatusCode == 202 { + injectClientCertError := consumeInjectClientCertError(auth.Config.InjectCertLogPath) + if injectClientCertError != "" { + log.Error(log.CAKC055, injectClientCertError) + } + } + span.RecordErrorAndSetStatus(err) + span.End() + return err + } + span.End() + + _, span = tracer.Start(ctx, "Load cert file") + // load client cert + certPEMBlock, err := ioutil.ReadFile(auth.Config.Common.ClientCertPath) + if err != nil { + span.RecordErrorAndSetStatus(err) + span.End() + + if os.IsNotExist(err) { + return log.RecordedError(log.CAKC011, auth.Config.Common.ClientCertPath) + } + + return log.RecordedError(log.CAKC012, err) + } + log.Debug(log.CAKC049, auth.Config.Common.ClientCertPath) + + certDERBlock, certPEMBlock := pem.Decode(certPEMBlock) + cert, err := x509.ParseCertificate(certDERBlock.Bytes) + if err != nil { + return log.RecordedError(log.CAKC013, auth.Config.Common.ClientCertPath, err) + span.RecordErrorAndSetStatus(err) + span.End() + + return log.RecordedError(log.CAKC013, auth.Config.Common.ClientCertPath, err) + } + + auth.PublicCert = cert + span.End() + + // clean up the client cert so it's only available in memory + os.Remove(auth.Config.Common.ClientCertPath) + log.Debug(log.CAKC050) + + return nil +} + +// IsLoggedIn returns true if we are logged in (have a cert) +func (auth *Authenticator) IsLoggedIn() bool { + return auth.PublicCert != nil +} + +// isCertExpired returns true if certificate is expired or close to expiring +func (auth *Authenticator) isCertExpired() bool { + certExpiresOn := auth.PublicCert.NotAfter.UTC() + currentDate := time.Now().UTC() + + log.Debug(log.CAKC042, certExpiresOn) + log.Debug(log.CAKC043, currentDate) + log.Debug(log.CAKC044, bufferTime) + + return currentDate.Add(bufferTime).After(certExpiresOn) +} + +// loginIfNeeded checks if we need to send a login request to Conjur and sends +// one if needed +func (auth *Authenticator) loginIfNeeded(ctx context.Context, tracer trace.Tracer) error { + if !auth.IsLoggedIn() { + log.Debug(log.CAKC039) + + if err := auth.login(ctx, tracer); err != nil { + return log.RecordedError(log.CAKC015) + } + + log.Debug(log.CAKC036) + } + + if auth.isCertExpired() { + log.Debug(log.CAKC038) + + if err := auth.login(ctx, tracer); err != nil { + return err + } + + log.Debug(log.CAKC037) + } + + return nil +} + +// sendAuthenticationRequest reads the cert from memory and uses it to send +// an authentication request to the Conjur server. It also validates the response +// code before returning its body +func (auth *Authenticator) sendAuthenticationRequest(ctx context.Context, tracer trace.Tracer) ([]byte, error) { + _, span := tracer.Start(ctx, "Send authentication request") + defer span.End() + + privDer := x509.MarshalPKCS1PrivateKey(auth.privateKey) + keyPEMBlock := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privDer}) + + certPEMBlock := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: auth.PublicCert.Raw}) + + client, err := common.NewHTTPSClient(auth.Config.Common.SSLCertificate, certPEMBlock, keyPEMBlock) + if err != nil { + span.RecordErrorAndSetStatus(err) + return nil, err + } + + req, err := AuthenticateRequest( + auth.Config.Common.URL, + auth.Config.ConjurVersion, + auth.Config.Common.Account, + auth.Config.Common.Username.FullUsername, + ) + if err != nil { + span.RecordErrorAndSetStatus(err) + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + span.RecordErrorAndSetStatus(err) + return nil, log.RecordedError(log.CAKC027, err) + } + + err = utils.ValidateResponse(resp) + if err != nil { + span.RecordErrorAndSetStatus(err) + return nil, err + } + + return utils.ReadResponseBody(resp) +} + +// parseAuthenticationResponse takes the response from the Authenticate +// request, decrypts if needed, and returns it +func (auth *Authenticator) parseAuthenticationResponse(ctx context.Context, tracer trace.Tracer, response []byte) ([]byte, error) { + _, span := tracer.Start(ctx, "Parse authentication response") + defer span.End() + + var content []byte + var err error + + // Token is only encrypted in Conjur v4 + if auth.Config.ConjurVersion == "4" { + content, err = decodeFromPEM(response, auth.PublicCert, auth.privateKey) + if err != nil { + span.RecordErrorAndSetStatus(err) + return nil, log.RecordedError(log.CAKC020) + } + } else if auth.Config.ConjurVersion == "5" { + content = response + } + + return content, nil +} + +// generateSANURI returns the formatted uri(SPIFFEE format for now) for the certificate. +func generateSANURI(namespace, podname string) (string, error) { + if namespace == "" || podname == "" { + return "", log.RecordedError(log.CAKC008, namespace, podname) + } + return fmt.Sprintf("spiffe://cluster.local/namespace/%s/podname/%s", namespace, podname), nil +} + +func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL) ([]byte, error) { + var rawValues []asn1.RawValue + for _, name := range dnsNames { + rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeDNS, Class: asn1.ClassContextSpecific, Bytes: []byte(name)}) + } + for _, email := range emailAddresses { + rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeEmail, Class: asn1.ClassContextSpecific, Bytes: []byte(email)}) + } + for _, rawIP := range ipAddresses { + // If possible, we always want to encode IPv4 addresses in 4 bytes. + ip := rawIP.To4() + if ip == nil { + ip = rawIP + } + rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeIP, Class: asn1.ClassContextSpecific, Bytes: ip}) + } + for _, uri := range uris { + rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeURI, Class: asn1.ClassContextSpecific, Bytes: []byte(uri.String())}) + } + return asn1.Marshal(rawValues) +} + +func decodeFromPEM(PEMBlock []byte, publicCert *x509.Certificate, privateKey crypto.PrivateKey) ([]byte, error) { + var decodedPEM []byte + + tokenDerBlock, _ := pem.Decode(PEMBlock) + p7, err := pkcs7.Parse(tokenDerBlock.Bytes) + if err != nil { + return nil, log.RecordedError(log.CAKC026, err) + } + + decodedPEM, err = p7.Decrypt(publicCert, privateKey) + if err != nil { + return nil, log.RecordedError(log.CAKC025, err) + } + + return decodedPEM, nil +} + +func consumeInjectClientCertError(path string) string { + // The log file will not exist in old Conjur versions + err := utils.VerifyFileExists(path) + if err != nil { + log.Warn(log.CAKC056, path) + return "" + } + + content, err := ioutil.ReadFile(path) + if err != nil { + log.Error(log.CAKC053, path) + return "" + } + + log.Debug(log.CAKC057, path) + err = os.Remove(path) + if err != nil { + log.Error(log.CAKC054, path) + } + + return string(content) +} diff --git a/pkg/authenticator/k8s/config.go b/pkg/authenticator/k8s/config.go new file mode 100644 index 00000000..b8535662 --- /dev/null +++ b/pkg/authenticator/k8s/config.go @@ -0,0 +1,133 @@ +package k8s + +import ( + "fmt" + "time" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/common" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" +) + +// Config defines the configuration parameters +// for the authentication requests +type Config struct { + Common common.Config + InjectCertLogPath string + PodName string + PodNamespace string + ConjurVersion string +} + +// Default settings (this comment added to satisfy linter) +const ( + DefaultClientCertPath = "/etc/conjur/ssl/client.pem" + DefaultInjectCertLogPath = "/tmp/conjur_copy_text_output.log" + DefaultTokenFilePath = "/run/conjur/access-token" + + DefaultConjurVersion = "5" + + // DefaultTokenRefreshTimeout is the default time the system waits to reauthenticate on error + DefaultTokenRefreshTimeout = "6m0s" + + // DefaultClientCertRetryCountLimit is the amount of times we wait after successful + // login for the client certificate file to exist, where each time we wait for a second. + DefaultClientCertRetryCountLimit = "10" + AuthnType = "authn-k8s" +) + +var requiredEnvVariables = []string{ + "CONJUR_AUTHN_URL", + "CONJUR_ACCOUNT", + "CONJUR_AUTHN_LOGIN", + "MY_POD_NAMESPACE", + "MY_POD_NAME", +} + +var envVariables = []string{ + "CONJUR_ACCOUNT", + "CONJUR_AUTHN_LOGIN", + "CONJUR_AUTHN_TOKEN_FILE", + "CONJUR_AUTHN_URL", + "CONJUR_CERT_FILE", + "CONJUR_CLIENT_CERT_PATH", + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT", + "CONJUR_SSL_CERTIFICATE", + "CONJUR_TOKEN_TIMEOUT", + "CONJUR_VERSION", + "CONTAINER_MODE", + "DEBUG", + "MY_POD_NAME", + "MY_POD_NAMESPACE", +} + +var defaultValues = map[string]string{ + "CONJUR_CLIENT_CERT_PATH": DefaultClientCertPath, + "CONJUR_AUTHN_TOKEN_FILE": DefaultTokenFilePath, + "CONJUR_VERSION": DefaultConjurVersion, + "CONJUR_TOKEN_TIMEOUT": DefaultTokenRefreshTimeout, + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": DefaultClientCertRetryCountLimit, +} + +func durationFromString(key, value string) (time.Duration, error) { + duration, err := time.ParseDuration(value) + if err != nil { + return 0, fmt.Errorf(log.CAKC060, key, value) + } + return duration, nil +} + +func configureLogLevel(level string) { + validVal := "true" + if level == validVal { + log.EnableDebugMode() + } else if level != "" { + // In case "DEBUG" is configured with incorrect value + log.Warn(log.CAKC034, level, validVal) + } +} + +func (config *Config) LoadConfig(settings map[string]string) { + configureLogLevel(settings["DEBUG"]) + + config.Common = common.Config{} + config.Common.LoadConfig(settings) + + for key, value := range settings { + switch key { + case "MY_POD_NAME": + config.PodName = value + case "MY_POD_NAMESPACE": + config.PodNamespace = value + case "CONJUR_VERSION": + config.ConjurVersion = value + } + } +} + +func (config *Config) GetAuthenticationType() string { + return AuthnType +} + +func (config *Config) GetEnvVariables() []string { + return envVariables +} + +func (config *Config) GetRequiredVariables() []string { + return requiredEnvVariables +} + +func (config *Config) GetDefaultValues() map[string]string { + return defaultValues +} + +func (config *Config) GetContainerMode() string { + return config.Common.ContainerMode +} + +func (config *Config) GetTokenFilePath() string { + return config.Common.TokenFilePath +} + +func (config *Config) GetTokenTimeout() time.Duration { + return config.Common.TokenRefreshTimeout +} diff --git a/pkg/authenticator/requests.go b/pkg/authenticator/k8s/requests.go similarity index 98% rename from pkg/authenticator/requests.go rename to pkg/authenticator/k8s/requests.go index f7f934e3..3869fe1f 100644 --- a/pkg/authenticator/requests.go +++ b/pkg/authenticator/k8s/requests.go @@ -1,4 +1,4 @@ -package authenticator +package k8s import ( "bytes" diff --git a/pkg/authenticator/requests_test.go b/pkg/authenticator/k8s/requests_test.go similarity index 95% rename from pkg/authenticator/requests_test.go rename to pkg/authenticator/k8s/requests_test.go index 0305b3ca..9b594a80 100644 --- a/pkg/authenticator/requests_test.go +++ b/pkg/authenticator/k8s/requests_test.go @@ -1,4 +1,4 @@ -package authenticator +package k8s import ( "testing" diff --git a/pkg/authenticator/authenticator_test.go b/pkg/authenticator/k8s/tests/authenticator_test.go similarity index 76% rename from pkg/authenticator/authenticator_test.go rename to pkg/authenticator/k8s/tests/authenticator_test.go index 1b63a6dd..d2bb1e6a 100644 --- a/pkg/authenticator/authenticator_test.go +++ b/pkg/authenticator/k8s/tests/authenticator_test.go @@ -1,4 +1,4 @@ -package authenticator +package tests import ( "bytes" @@ -13,12 +13,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/cyberark/conjur-authn-k8s-client/pkg/access_token/memory" - "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/common" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" "github.com/cyberark/conjur-authn-k8s-client/pkg/log" ) type assertFunc func(t *testing.T, - authn *Authenticator, + authn *k8s.Authenticator, err error, loginCsr *x509.CertificateRequest, loginCsrErr error, @@ -37,7 +38,7 @@ func TestAuthenticator_Authenticate(t *testing.T) { name: "happy path", podName: "testPodName", podNamespace: "testPodNamespace", - assert: func(t *testing.T, authn *Authenticator, err error, loginCsr *x509.CertificateRequest, loginCsrErr error, _ string) { + assert: func(t *testing.T, authn *k8s.Authenticator, err error, loginCsr *x509.CertificateRequest, loginCsrErr error, _ string) { assert.NoError(t, err) // Check the CSR @@ -68,7 +69,7 @@ func TestAuthenticator_Authenticate(t *testing.T) { name: "empty podname", podName: "", podNamespace: "", - assert: func(t *testing.T, authn *Authenticator, err error, loginCsr *x509.CertificateRequest, _ error, _ string) { + assert: func(t *testing.T, authn *k8s.Authenticator, err error, loginCsr *x509.CertificateRequest, _ error, _ string) { assert.NoError(t, err) // Assert empty spiffe @@ -83,7 +84,7 @@ func TestAuthenticator_Authenticate(t *testing.T) { name: "expired cert", podName: "testPodName", podNamespace: "testPodNamespace", - assert: func(t *testing.T, authn *Authenticator, err error, _ *x509.CertificateRequest, _ error, _ string) { + assert: func(t *testing.T, authn *k8s.Authenticator, err error, _ *x509.CertificateRequest, _ error, _ string) { assert.NoError(t, err) // Set the expiration date to now, and try to authenticate again // This will cause the authenticator to try to refresh the cert @@ -100,7 +101,7 @@ func TestAuthenticator_Authenticate(t *testing.T) { podName: "testPodName", podNamespace: "testPodNamespace", skipWritingCSRFile: true, - assert: func(t *testing.T, _ *Authenticator, err error, _ *x509.CertificateRequest, _ error, logTxt string) { + assert: func(t *testing.T, _ *k8s.Authenticator, err error, _ *x509.CertificateRequest, _ error, logTxt string) { assert.Error(t, err) // Check logs for the expected error assert.Contains(t, logTxt, "error writing csr file") @@ -133,26 +134,28 @@ func TestAuthenticator_Authenticate(t *testing.T) { Type: "CERTIFICATE", Bytes: ts.Server.Certificate().Raw, }) - username, _ := config.NewUsername("host/test-user") - - cfg := config.Config{ - Account: "account", - ClientCertPath: clientCertPath, - ClientCertRetryCountLimit: 0, - ContainerMode: "doesntmatter", - ConjurVersion: "5", - InjectCertLogPath: certLogPath, - PodName: tc.podName, - PodNamespace: tc.podNamespace, - SSLCertificate: sslcert, - TokenFilePath: tokenPath, - TokenRefreshTimeout: 0, - URL: ts.Server.URL, - Username: username, + username, _ := common.NewUsername("host/test-user") + + cfg := k8s.Config{ + ConjurVersion: "5", + InjectCertLogPath: certLogPath, + PodName: tc.podName, + PodNamespace: tc.podNamespace, + Common: common.Config{ + SSLCertificate: sslcert, + TokenFilePath: tokenPath, + TokenRefreshTimeout: 0, + URL: ts.Server.URL, + Username: username, + Account: "account", + ClientCertPath: clientCertPath, + ClientCertRetryCountLimit: 0, + ContainerMode: "doesntmatter", + }, } // EXERCISE - authn, err := NewWithAccessToken(cfg, at) + authn, err := k8s.NewWithAccessToken(cfg, at) if !assert.NoError(t, err) { return } diff --git a/pkg/authenticator/authenticator_test_server.go b/pkg/authenticator/k8s/tests/authenticator_test_server.go similarity index 99% rename from pkg/authenticator/authenticator_test_server.go rename to pkg/authenticator/k8s/tests/authenticator_test_server.go index 9eb0de15..bbe69eeb 100644 --- a/pkg/authenticator/authenticator_test_server.go +++ b/pkg/authenticator/k8s/tests/authenticator_test_server.go @@ -1,4 +1,4 @@ -package authenticator +package tests import ( "crypto/rand" diff --git a/pkg/authenticator/k8s/tests/config_test.go b/pkg/authenticator/k8s/tests/config_test.go new file mode 100644 index 00000000..b092a072 --- /dev/null +++ b/pkg/authenticator/k8s/tests/config_test.go @@ -0,0 +1,171 @@ +package tests + +import ( + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" + + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +var environmentValues = map[string]string{ + "CONJUR_AUTHN_URL": "filepath", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_LOGIN": "host", + "CONJUR_CERT_FILE": "testSSLCertFile.txt", + "CONJUR_SSL_CERTIFICATE": "testSSLCert", + "CONJUR_VERSION": "", + "MY_POD_NAMESPACE": "testNameSpace", + "MY_POD_NAME": "testPodName", +} + +var annotationValues = map[string]string{ + "conjur.org/authn-identity": "host/anotherHost", + "conjur.org/debug-logging": "true", + "conjur.org/container-mode": "init", +} + +var envToAnnot = map[string]string{ + "CONJUR_AUTHN_LOGIN": "conjur.org/authn-identity", + "DEBUG": "conjur.org/debug-logging", + "CONTAINER_MODE": "conjur.org/container-mode", +} + +func assertGoodConfig(expected *k8s.Config) func(*testing.T, *k8s.Config) { + return func(t *testing.T, result *k8s.Config) { + assert.Equal(t, expected, result) + } +} + +type errorAssertFunc func(*testing.T, []error) + +func setEnv(env map[string]string) { + for key, value := range env { + os.Setenv(key, value) + } +} + +func unsetEnv(env map[string]string) { + for key := range env { + os.Setenv(key, "") + } +} + +func TestGatherSettings(t *testing.T) { + TestCases := []struct { + description string + annotFunc func(string) string + expected config.AuthnSettings + }{ + { + description: "functions are ordered by priority: first function overrides second, which overrides third", + annotFunc: fromAnnotations, + expected: config.AuthnSettings{ + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_LOGIN": "host/anotherHost", // provided by annotation + "CONJUR_AUTHN_URL": "filepath", + "CONJUR_CERT_FILE": "testSSLCertFile.txt", + "CONJUR_SSL_CERTIFICATE": "testSSLCert", + "CONTAINER_MODE": "init", // provided by annotation + "DEBUG": "true", // provided by annotation + "MY_POD_NAME": "testPodName", + "MY_POD_NAMESPACE": "testNameSpace", + "CONJUR_AUTHN_TOKEN_FILE": k8s.DefaultTokenFilePath, + "CONJUR_CLIENT_CERT_PATH": k8s.DefaultClientCertPath, + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": k8s.DefaultClientCertRetryCountLimit, + "CONJUR_TOKEN_TIMEOUT": k8s.DefaultTokenRefreshTimeout, + "CONJUR_VERSION": k8s.DefaultConjurVersion, + }, + }, + { + description: "if the first getter function returns empty strings, fallback to the next functions, and eventually an empty string", + annotFunc: emptyAnnotations, + expected: config.AuthnSettings{ + "CONJUR_AUTHN_URL": "filepath", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_LOGIN": "host", + "CONJUR_CERT_FILE": "testSSLCertFile.txt", + "CONJUR_SSL_CERTIFICATE": "testSSLCert", + "MY_POD_NAMESPACE": "testNameSpace", + "MY_POD_NAME": "testPodName", + "DEBUG": "", + "CONTAINER_MODE": "", + "CONJUR_CLIENT_CERT_PATH": k8s.DefaultClientCertPath, + "CONJUR_AUTHN_TOKEN_FILE": k8s.DefaultTokenFilePath, + "CONJUR_VERSION": k8s.DefaultConjurVersion, + "CONJUR_TOKEN_TIMEOUT": k8s.DefaultTokenRefreshTimeout, + "CONJUR_CLIENT_CERT_RETRY_COUNT_LIMIT": k8s.DefaultClientCertRetryCountLimit, + }, + }, + } + + for _, tc := range TestCases { + t.Run(tc.description, func(t *testing.T) { + resultMap := config.GatherSettings(&k8s.Config{}, tc.annotFunc, fromEnv) + assert.Equal(t, tc.expected, resultMap) + }) + } +} + +func TestFromEnv(t *testing.T) { + TestCases := []struct { + description string + env map[string]string + expectErr bool + }{ + { + description: "happy path", + env: map[string]string{ + "CONJUR_AUTHN_URL": "authn-k8s", + "CONJUR_ACCOUNT": "testAccount", + "CONJUR_AUTHN_LOGIN": "host/test-user", + "MY_POD_NAME": "testPodName", + "MY_POD_NAMESPACE": "testNameSpace", + "CONJUR_SSL_CERTIFICATE": "samplecert", + }, + expectErr: false, + }, + { + description: "bad settings return nil Config and error message", + env: map[string]string{}, + expectErr: true, + }, + } + + for _, tc := range TestCases { + t.Run(tc.description, func(t *testing.T) { + // SETUP & EXERCISE + setEnv(tc.env) + config, err := config.ConfigFromEnv(successfulMockReadFile) + + // ASSERT + if tc.expectErr { + assert.Nil(t, config) + assert.NotNil(t, err) + } else { + assert.NotNil(t, config) + assert.Nil(t, err) + } + unsetEnv(tc.env) + }) + } +} + +func successfulMockReadFile(filename string) ([]byte, error) { + return []byte{}, nil +} + +func fromEnv(key string) string { + return environmentValues[key] +} + +func fromAnnotations(key string) string { + annot := envToAnnot[key] + return annotationValues[annot] +} + +func emptyAnnotations(key string) string { + return "" +} diff --git a/pkg/log/log_messages.go b/pkg/log/log_messages.go index 10d19ed9..0ca30c30 100644 --- a/pkg/log/log_messages.go +++ b/pkg/log/log_messages.go @@ -75,3 +75,5 @@ const CAKC059 string = "CAKC059 Path exists but does not contain regular file: % const CAKC060 string = "CAKC060 Setting %s given invalid value %s" const CAKC061 string = "CAKC061 Failed to validate setting for Authenticator configuration" const CAKC062 string = "CAKC062 Required Authenticator setting %s not provided" +const CAKC063 string = "CAKC063 Unable to find configuration for URL: %s" +const CAKC064 string = "CAKC064 Unable to find Authenticator for this configuration"