diff --git a/cmd/authenticator/main.go b/cmd/authenticator/main.go index 97f4c014..f0b7a31c 100644 --- a/cmd/authenticator/main.go +++ b/cmd/authenticator/main.go @@ -8,7 +8,6 @@ import ( "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" ) @@ -17,15 +16,9 @@ func main() { var err error - config, err := authnConfig.NewFromEnv() - if err != nil { - printErrorAndExit(log.CAKC018) - } - - // Create new Authenticator - authn, err := authenticator.New(*config) - if err != nil { - printErrorAndExit(log.CAKC019) + authn, errMsg := authenticator.NewAuthenticatorFromEnv() + if errMsg != "" { + printErrorAndExit(errMsg) } // Configure exponential backoff @@ -43,14 +36,14 @@ func main() { return log.RecordedError(log.CAKC016) } - if authn.Config.ContainerMode == "init" { + if authn.GlobalConfig().ContainerMode == "init" { os.Exit(0) } - log.Info(log.CAKC047, authn.Config.TokenRefreshTimeout) + log.Info(log.CAKC047, authn.GlobalConfig().TokenRefreshTimeout) fmt.Println() - time.Sleep(authn.Config.TokenRefreshTimeout) + time.Sleep(authn.GlobalConfig().TokenRefreshTimeout) // Reset exponential backoff expBackoff.Reset() diff --git a/pkg/authenticator/authenticator.go b/pkg/authenticator/authenticator.go index 815ca36e..8c97659d 100644 --- a/pkg/authenticator/authenticator.go +++ b/pkg/authenticator/authenticator.go @@ -1,383 +1,62 @@ package authenticator import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/pem" "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" "os" - "time" + "strings" - "github.com/fullsailor/pkcs7" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/common" + gcpAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/gcp" - "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" + k8sAuthenticator "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s" "github.com/cyberark/conjur-authn-k8s-client/pkg/log" - "github.com/cyberark/conjur-authn-k8s-client/pkg/utils" ) -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 + // DefaultAuthnType defaults to k8s authentication + DefaultAuthnType = k8sAuthenticator.AuthnType + // AuthnTypeEnvironmentVariable environment variable set for authentication type + AuthnTypeEnvironmentVariable = "CONJUR_AUTHN_TYPE" ) -// 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 -} - -// 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() error { - - log.Debug(log.CAKC041, auth.Config.Username) - - csrRawBytes, err := auth.GenerateCSR(auth.Config.Username.Suffix) - - csrBytes := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE REQUEST", Bytes: csrRawBytes, - }) - - req, err := LoginRequest(auth.Config.URL, auth.Config.ConjurVersion, csrBytes, auth.Config.Username.Prefix) - if err != nil { - return err - } - - resp, err := auth.client.Do(req) - if err != nil { - return log.RecordedError(log.CAKC028, err) - } - - err = utils.ValidateResponse(resp) - if err != nil { - return log.RecordedError(log.CAKC029, err) - } - - // Ensure client certificate exists before attempting to read it, with a tolerance - // for small delays - err = utils.WaitForFile( - auth.Config.ClientCertPath, - auth.Config.ClientCertRetryCountLimit, - nil, - ) - 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) - } - } - return err - } - - // load client cert - certPEMBlock, err := ioutil.ReadFile(auth.Config.ClientCertPath) - if err != nil { - 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 { - return log.RecordedError(log.CAKC013, auth.Config.ClientCertPath, err) - } - - auth.PublicCert = cert - - // 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) -} - -// 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) Authenticate() error { - log.Info(log.CAKC040, auth.Config.Username) - - err := auth.loginIfNeeded() - if err != nil { - return err - } - - authenticationResponse, err := auth.sendAuthenticationRequest() - if err != nil { - return err - } - - parsedResponse, err := auth.parseAuthenticationResponse(authenticationResponse) - if err != nil { - return err - } - - err = auth.AccessToken.Write(parsedResponse) - if err != nil { - return err - } - - log.Info(log.CAKC035) - return nil -} - -// loginIfNeeded checks if we need to send a login request to Conjur and sends -// one if needed -func (auth *Authenticator) loginIfNeeded() error { - if !auth.IsLoggedIn() { - log.Debug(log.CAKC039) - - if err := auth.Login(); err != nil { - return log.RecordedError(log.CAKC015) - } - - log.Debug(log.CAKC036) - } - - if auth.IsCertExpired() { - log.Debug(log.CAKC038) - - if err := auth.Login(); 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() ([]byte, error) { - 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}) +// NewAuthenticatorFromEnv returns desired authenticator type +func NewAuthenticatorFromEnv() (common.Authenticator, string) { + configureLogLevel() - client, err := newHTTPSClient(auth.Config.SSLCertificate, certPEMBlock, keyPEMBlock) - if err != nil { - return nil, err - } - - req, err := AuthenticateRequest( - auth.Config.URL, - auth.Config.ConjurVersion, - auth.Config.Account, - auth.Config.Username.FullUsername, - ) - if err != nil { - return nil, err - } - - resp, err := client.Do(req) - if err != nil { - return nil, log.RecordedError(log.CAKC027, err) - } - - err = utils.ValidateResponse(resp) - if err != nil { - return nil, err - } + authnStategies := registeredAuthenticators() + authnType := getAuthnType() - return utils.ReadResponseBody(resp) -} - -// parseAuthenticationResponse takes the response from the Authenticate -// request, decrypts if needed, and returns it -func (auth *Authenticator) parseAuthenticationResponse(response []byte) ([]byte, error) { - 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 { - return nil, log.RecordedError(log.CAKC020) + for _, authnStategy := range authnStategies { + if authnStategy.CanHandle(authnType) { + return authnStategy.Init() } - } 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 + return nil, fmt.Sprintf(log.CAKC060, authnType) } -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)}) +func configureLogLevel() { + validVal := "true" + val := os.Getenv("DEBUG") + if val == validVal { + log.EnableDebugMode() + } else if val != "" { + // In case "DEBUG" is configured with incorrect value + log.Warn(log.CAKC034, val, validVal) } - 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) +func registeredAuthenticators() []common.Authenticator { + return []common.Authenticator{ + &gcpAuthenticator.Authenticator{}, + &k8sAuthenticator.Authenticator{}, } - - 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 "" +func getAuthnType() string { + authnType := os.Getenv(AuthnTypeEnvironmentVariable) + if authnType == "" { + return DefaultAuthnType } - - log.Debug(log.CAKC057, path) - err = os.Remove(path) - if err != nil { - log.Error(log.CAKC054, path) - } - - return string(content) + return strings.ToLower(authnType) } diff --git a/pkg/authenticator/common/authenticator.go b/pkg/authenticator/common/authenticator.go new file mode 100644 index 00000000..adbaa846 --- /dev/null +++ b/pkg/authenticator/common/authenticator.go @@ -0,0 +1,11 @@ +package common + +import "github.com/cyberark/conjur-authn-k8s-client/pkg/config" + +// Authenticator represents an authenticator interface +type Authenticator interface { + Authenticate() error + GlobalConfig() config.Config + Init() (Authenticator, string) + CanHandle(string) bool +} diff --git a/pkg/authenticator/gcp/authenticator.go b/pkg/authenticator/gcp/authenticator.go new file mode 100644 index 00000000..c8020c66 --- /dev/null +++ b/pkg/authenticator/gcp/authenticator.go @@ -0,0 +1,169 @@ +package gcp + +import ( + "crypto/x509" + "net/http" + "os" + "strings" + + "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/common" + authnConfig "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/gcp/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" + "github.com/cyberark/conjur-authn-k8s-client/pkg/utils" +) + +const ( + // AuthnType is gcp for google cloud authentication + AuthnType = "gcp" +) + +// Authenticator contains the configuration and client +// for the authentication connection to Conjur +type Authenticator struct { + client *http.Client + AccessToken access_token.AccessToken + Config authnConfig.Config + PublicCert *x509.Certificate +} + +// GlobalConfig returns config used in the cmd package +func (auth *Authenticator) GlobalConfig() config.Config { + return config.Config{ + TokenRefreshTimeout: auth.Config.TokenRefreshTimeout, + ContainerMode: auth.Config.ContainerMode, + } +} + +// Init returns config used in the cmd package +func (auth *Authenticator) Init() (common.Authenticator, string) { + log.Debug(log.CAKC059) + config, err := authnConfig.NewFromEnv() + if err != nil { + return nil, log.CAKC018 + } + + authn, err := New(*config) + if err != nil { + return nil, log.CAKC019 + } + + return authn, "" +} + +// CanHandle returns true if provided string is 'gcp' +func (auth *Authenticator) CanHandle(authnType string) bool { + return strings.ToLower(authnType) == AuthnType +} + +// 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) { + client, err := newHTTPSClient(config.SSLCertificate) + if err != nil { + return nil, err + } + + return &Authenticator{ + client: client, + AccessToken: accessToken, + Config: config, + }, nil +} + +// Authenticate sends Conjur an authenticate request and writes the response +// to the token file +func (auth *Authenticator) Authenticate() error { + log.Info(log.CAKC040, auth.Config.Username) + + sessionToken, err := auth.sendMetadataRequest() + if err != nil { + return err + } + + authenticationResponse, err := auth.sendAuthenticationRequest(sessionToken) + if err != nil { + return err + } + + err = auth.AccessToken.Write(authenticationResponse) + if err != nil { + return err + } + + log.Info(log.CAKC035) + return nil +} + +// sendAuthenticationRequest sends the google service account session token +// to the conjur authn url +func (auth *Authenticator) sendAuthenticationRequest(sessionToken []byte) ([]byte, error) { + client, err := newHTTPSClient(auth.Config.SSLCertificate) + if err != nil { + return nil, err + } + + base64Token := strings.ToLower(os.Getenv("CONJUR_BASE64_TOKEN")) == "true" + + req, err := AuthenticateRequest( + auth.Config.URL, + auth.Config.Account, + sessionToken, + base64Token, + ) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, log.RecordedError(log.CAKC027, err) + } + + err = utils.ValidateResponse(resp) + if err != nil { + return nil, err + } + + return utils.ReadResponseBody(resp) +} + +// sendMetadataRequest sends the get google service account to the +// google metadata url and returns the service account session token +func (auth *Authenticator) sendMetadataRequest() ([]byte, error) { + client, err := newHTTPSClient(auth.Config.SSLCertificate) + if err != nil { + return nil, err + } + + req, err := MetadataRequest( + auth.Config.Account, + auth.Config.Username, + ) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, log.RecordedError(log.CAKC027, err) + } + + err = utils.ValidateResponse(resp) + if err != nil { + return nil, err + } + + return utils.ReadResponseBody(resp) +} diff --git a/pkg/authenticator/gcp/client.go b/pkg/authenticator/gcp/client.go new file mode 100644 index 00000000..2b023b6c --- /dev/null +++ b/pkg/authenticator/gcp/client.go @@ -0,0 +1,29 @@ +package gcp + +import ( + "crypto/tls" + "crypto/x509" + "net/http" + "time" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" +) + +func newHTTPSClient(CACert []byte) (*http.Client, error) { + caCertPool := x509.NewCertPool() + ok := caCertPool.AppendCertsFromPEM(CACert) + if !ok { + return nil, log.RecordedError(log.CAKC014) + } + + // Setup HTTPS client + tlsConfig := &tls.Config{ + RootCAs: caCertPool, + } + + // Doubt this is necessary because there's only one + //tlsConfig.BuildNameToCertificate() + transport := &http.Transport{TLSClientConfig: tlsConfig} + + return &http.Client{Transport: transport, Timeout: time.Second * 10}, nil +} diff --git a/pkg/authenticator/gcp/config/config.go b/pkg/authenticator/gcp/config/config.go new file mode 100644 index 00000000..372b0462 --- /dev/null +++ b/pkg/authenticator/gcp/config/config.go @@ -0,0 +1,114 @@ +package config + +import ( + "io/ioutil" + "os" + "time" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" + "github.com/cyberark/conjur-authn-k8s-client/pkg/utils" +) + +// Config defines the configuration parameters +// for the authentication requests +type Config struct { + Account string + ContainerMode string + SSLCertificate []byte + TokenFilePath string + TokenRefreshTimeout time.Duration + URL string + Username string +} + +// Default settings (this comment added to satisfy linter) +const ( + DefaultTokenFilePath = "/run/conjur/access-token" + + // DefaultTokenRefreshTimeout is the default time the system waits to reauthenticate on error + DefaultTokenRefreshTimeout = "6m0s" +) + +var requiredEnvVariables = []string{ + "CONJUR_APPLIANCE_URL", + "CONJUR_ACCOUNT", + "CONJUR_AUTHN_LOGIN", +} + +// 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) { + var err error + + // Fill config with 'simple' values from environment + config, err := populateConfig() + if err != nil { + return nil, err + } + + // Load CA cert from Environment + config.SSLCertificate, err = readSSLCert(readFileFunc) + if err != nil { + return nil, log.RecordedError(log.CAKC021, err) + } + + return config, nil +} + +func populateConfig() (*Config, error) { + // Check that required environment variables are set + for _, envvar := range requiredEnvVariables { + if os.Getenv(envvar) == "" { + return nil, log.RecordedError(log.CAKC009, envvar) + } + } + + config := &Config{ + Account: os.Getenv("CONJUR_ACCOUNT"), + ContainerMode: os.Getenv("CONTAINER_MODE"), + URL: os.Getenv("CONJUR_APPLIANCE_URL"), + Username: os.Getenv("CONJUR_AUTHN_LOGIN"), + } + + // Parse token refresh rate if one is provided from env + tokenRefreshTimeout, err := utils.DurationFromEnvOrDefault( + "CONJUR_TOKEN_TIMEOUT", + DefaultTokenRefreshTimeout, + nil, + ) + if err != nil { + return nil, err + } + config.TokenRefreshTimeout = tokenRefreshTimeout + + config.TokenFilePath = DefaultTokenFilePath + // If CONJUR_TOKEN_FILE_PATH is defined in the env we take its value + if envVal := os.Getenv("CONJUR_AUTHN_TOKEN_FILE"); envVal != "" { + config.TokenFilePath = envVal + } + + // Load Username from Environment + config.Username = os.Getenv("CONJUR_AUTHN_LOGIN") + + return config, nil +} + +func readSSLCert(readFile ReadFileFunc) ([]byte, error) { + SSLCert := os.Getenv("CONJUR_SSL_CERTIFICATE") + SSLCertPath := os.Getenv("CONJUR_CERT_FILE") + if SSLCert == "" && SSLCertPath == "" { + return nil, log.RecordedError(log.CAKC007) + } + + if SSLCert != "" { + return []byte(SSLCert), nil + } + return readFile(SSLCertPath) +} diff --git a/pkg/authenticator/gcp/request.go b/pkg/authenticator/gcp/request.go new file mode 100644 index 00000000..b5373b98 --- /dev/null +++ b/pkg/authenticator/gcp/request.go @@ -0,0 +1,53 @@ +package gcp + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" +) + +// AuthenticateRequest sends an authenticate request +func AuthenticateRequest(applianceURL string, account string, sessionToken []byte, base64Token bool) (*http.Request, error) { + var authenticateURL string + var err error + var req *http.Request + + authenticateURL = fmt.Sprintf("%s/authn-gcp/%s/authenticate", applianceURL, account) + log.Debug(log.CAKC046, authenticateURL) + + body := strings.NewReader(fmt.Sprintf("jwt=%s", string(sessionToken))) + + if req, err = http.NewRequest("POST", authenticateURL, body); err != nil { + return nil, log.RecordedError(log.CAKC023, err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if base64Token { + req.Header.Set("Accept-Encoding", "base64") + } + + return req, nil +} + +// MetadataRequest sends a request to the google metadata +// endpoint to get a service account bearer token +func MetadataRequest(account string, username string) (*http.Request, error) { + var err error + var req *http.Request + + metadataIdentityURL := "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity" + audience := url.QueryEscape(fmt.Sprintf("conjur/%s/%s", account, username)) + metadataURL := fmt.Sprintf("%s?audience=%s&format=full", metadataIdentityURL, audience) + log.Debug(log.CAKC046, metadataURL) + + if req, err = http.NewRequest("GET", metadataURL, nil); err != nil { + return nil, log.RecordedError(log.CAKC023, err) + } + + req.Header.Set("Metadata-Flavor", "Google") + + return req, nil +} diff --git a/pkg/authenticator/k8s/authenticator.go b/pkg/authenticator/k8s/authenticator.go new file mode 100644 index 00000000..30ea7995 --- /dev/null +++ b/pkg/authenticator/k8s/authenticator.go @@ -0,0 +1,420 @@ +package k8s + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/fullsailor/pkcs7" + + "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/common" + authnConfig "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/log" + "github.com/cyberark/conjur-authn-k8s-client/pkg/utils" +) + +const ( + // AuthnType is k8s for kubernetes authentication + AuthnType = "k8s" +) + +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 +) + +// GlobalConfig returns config used in the cmd package +func (auth *Authenticator) GlobalConfig() config.Config { + return config.Config{ + TokenRefreshTimeout: auth.Config.TokenRefreshTimeout, + ContainerMode: auth.Config.ContainerMode, + } +} + +// Init the authenticator struct +func (auth *Authenticator) Init() (common.Authenticator, string) { + log.Debug(log.CAKC058) + config, err := authnConfig.NewFromEnv() + if err != nil { + return nil, log.CAKC018 + } + + authn, err := New(*config) + if err != nil { + return nil, log.CAKC019 + } + + return authn, "" +} + +// CanHandle returns true if provided string is 'AuthnType' +func (auth *Authenticator) CanHandle(authnType string) bool { + return strings.ToLower(authnType) == AuthnType +} + +// 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 +} + +// 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() error { + + log.Debug(log.CAKC041, auth.Config.Username) + + csrRawBytes, err := auth.GenerateCSR(auth.Config.Username.Suffix) + + csrBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", Bytes: csrRawBytes, + }) + + req, err := LoginRequest(auth.Config.URL, auth.Config.ConjurVersion, csrBytes, auth.Config.Username.Prefix) + if err != nil { + return err + } + + resp, err := auth.client.Do(req) + if err != nil { + return log.RecordedError(log.CAKC028, err) + } + + err = utils.ValidateResponse(resp) + if err != nil { + return log.RecordedError(log.CAKC029, err) + } + + // Ensure client certificate exists before attempting to read it, with a tolerance + // for small delays + err = utils.WaitForFile( + auth.Config.ClientCertPath, + auth.Config.ClientCertRetryCountLimit, + nil, + ) + 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) + } + } + return err + } + + // load client cert + certPEMBlock, err := ioutil.ReadFile(auth.Config.ClientCertPath) + if err != nil { + 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 { + return log.RecordedError(log.CAKC013, auth.Config.ClientCertPath, err) + } + + auth.PublicCert = cert + + // 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) +} + +// 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) Authenticate() error { + log.Info(log.CAKC040, auth.Config.Username) + + err := auth.loginIfNeeded() + if err != nil { + return err + } + + authenticationResponse, err := auth.sendAuthenticationRequest() + if err != nil { + return err + } + + parsedResponse, err := auth.parseAuthenticationResponse(authenticationResponse) + if err != nil { + return err + } + + err = auth.AccessToken.Write(parsedResponse) + if err != nil { + return err + } + + log.Info(log.CAKC035) + return nil +} + +// loginIfNeeded checks if we need to send a login request to Conjur and sends +// one if needed +func (auth *Authenticator) loginIfNeeded() error { + if !auth.IsLoggedIn() { + log.Debug(log.CAKC039) + + if err := auth.Login(); err != nil { + return log.RecordedError(log.CAKC015) + } + + log.Debug(log.CAKC036) + } + + if auth.IsCertExpired() { + log.Debug(log.CAKC038) + + if err := auth.Login(); 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() ([]byte, error) { + 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 { + return nil, err + } + + req, err := AuthenticateRequest( + auth.Config.URL, + auth.Config.ConjurVersion, + auth.Config.Account, + auth.Config.Username.FullUsername, + ) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, log.RecordedError(log.CAKC027, err) + } + + err = utils.ValidateResponse(resp) + if err != nil { + 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(response []byte) ([]byte, error) { + 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 { + 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/authenticator_test.go b/pkg/authenticator/k8s/authenticator_test.go similarity index 98% rename from pkg/authenticator/authenticator_test.go rename to pkg/authenticator/k8s/authenticator_test.go index 99e91ceb..ce118732 100644 --- a/pkg/authenticator/authenticator_test.go +++ b/pkg/authenticator/k8s/authenticator_test.go @@ -1,4 +1,4 @@ -package authenticator +package k8s import ( "crypto/rand" @@ -11,7 +11,7 @@ import ( . "github.com/smartystreets/goconvey/convey" - "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/config" + "github.com/cyberark/conjur-authn-k8s-client/pkg/authenticator/k8s/config" ) func parseCert(filename string) (*x509.Certificate, error) { diff --git a/pkg/authenticator/client.go b/pkg/authenticator/k8s/client.go similarity index 97% rename from pkg/authenticator/client.go rename to pkg/authenticator/k8s/client.go index 0c66103a..910fe075 100644 --- a/pkg/authenticator/client.go +++ b/pkg/authenticator/k8s/client.go @@ -1,4 +1,4 @@ -package authenticator +package k8s import ( "crypto/tls" diff --git a/pkg/authenticator/config/config.go b/pkg/authenticator/k8s/config/config.go similarity index 94% rename from pkg/authenticator/config/config.go rename to pkg/authenticator/k8s/config/config.go index e21ee46b..9e08f680 100644 --- a/pkg/authenticator/config/config.go +++ b/pkg/authenticator/k8s/config/config.go @@ -64,8 +64,6 @@ func NewFromEnv() (*Config, error) { func FromEnv(readFileFunc ReadFileFunc) (*Config, error) { var err error - configureLogLevel() - // Fill config with 'simple' values from environment config, err := populateConfig() if err != nil { @@ -87,17 +85,6 @@ func FromEnv(readFileFunc ReadFileFunc) (*Config, error) { return config, nil } -func configureLogLevel() { - validVal := "true" - val := os.Getenv("DEBUG") - if val == validVal { - log.EnableDebugMode() - } else if val != "" { - // In case "DEBUG" is configured with incorrect value - log.Warn(log.CAKC034, val, validVal) - } -} - func readSSLCert(readFile ReadFileFunc) ([]byte, error) { SSLCert := os.Getenv("CONJUR_SSL_CERTIFICATE") SSLCertPath := os.Getenv("CONJUR_CERT_FILE") diff --git a/pkg/authenticator/config/config_test.go b/pkg/authenticator/k8s/config/config_test.go similarity index 100% rename from pkg/authenticator/config/config_test.go rename to pkg/authenticator/k8s/config/config_test.go diff --git a/pkg/authenticator/config/username.go b/pkg/authenticator/k8s/config/username.go similarity index 100% rename from pkg/authenticator/config/username.go rename to pkg/authenticator/k8s/config/username.go diff --git a/pkg/authenticator/config/username_test.go b/pkg/authenticator/k8s/config/username_test.go similarity index 100% rename from pkg/authenticator/config/username_test.go rename to pkg/authenticator/k8s/config/username_test.go 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 96% rename from pkg/authenticator/requests_test.go rename to pkg/authenticator/k8s/requests_test.go index a7da9b34..e36b0200 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/testdata/example.csr b/pkg/authenticator/k8s/testdata/example.csr similarity index 100% rename from pkg/authenticator/testdata/example.csr rename to pkg/authenticator/k8s/testdata/example.csr diff --git a/pkg/authenticator/testdata/expired_cert.crt b/pkg/authenticator/k8s/testdata/expired_cert.crt similarity index 100% rename from pkg/authenticator/testdata/expired_cert.crt rename to pkg/authenticator/k8s/testdata/expired_cert.crt diff --git a/pkg/authenticator/testdata/good_cert.crt b/pkg/authenticator/k8s/testdata/good_cert.crt similarity index 100% rename from pkg/authenticator/testdata/good_cert.crt rename to pkg/authenticator/k8s/testdata/good_cert.crt diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 00000000..bdd034c1 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,9 @@ +package config + +import "time" + +// Config global configs used by the authenticator +type Config struct { + TokenRefreshTimeout time.Duration + ContainerMode string +} diff --git a/pkg/log/log_messages.go b/pkg/log/log_messages.go index c6228b20..94dcedb8 100644 --- a/pkg/log/log_messages.go +++ b/pkg/log/log_messages.go @@ -60,7 +60,7 @@ const CAKC044 string = "CAKC044 Buffer time: %v" const CAKC045 string = "CAKC045 Login request to: %s" const CAKC046 string = "CAKC046 Authn request to: %s" const CAKC047 string = "CAKC047 Waiting for %s to re-authenticate" -const CAKC048 string = "CAKC048 Kubernetes Authenticator Client v%s starting up..." +const CAKC048 string = "CAKC048 Conjur Authenticator Client v%s starting up..." const CAKC049 string = "CAKC049 Loaded client certificate successfully from %s" const CAKC050 string = "CAKC050 Deleted client certificate from memory" const CAKC051 string = "CAKC051 Waiting for file %s to become available..." @@ -70,3 +70,6 @@ const CAKC054 string = "CAKC054 Failed to delete file %s" const CAKC055 string = "CAKC055 Cert placement failed with the following error:\n%s" const CAKC056 string = "CAKC056 File %s does not exist" const CAKC057 string = "CAKC057 Removing file %s" +const CAKC058 string = "CAKC058 Initializing k8s authenticator" +const CAKC059 string = "CAKC059 Initializing gcp authenticator" +const CAKC060 string = "CAKC060 Invalid conjur authentication type '%s'"