diff --git a/examples/playbook/sample.tlspc.svc-account.yaml b/examples/playbook/sample.tlspc.svc-account.yaml new file mode 100644 index 00000000..07965f09 --- /dev/null +++ b/examples/playbook/sample.tlspc.svc-account.yaml @@ -0,0 +1,30 @@ +config: + connection: + platform: TLSPC # alternatively, VAAS can be used + credentials: + tenantId: '{{ Env "TLSPC_TENANT_ID" }}' # TLSPC tenant ID as environment variable + externalJWT: '{{ Env "TLSPC_EXTERNAL_JWT" }}' # JWT from Identity Provider as environment variable + #externalJWT: 'file:/path/to/jwt' # JWT from Identity Provider as file +certificateTasks: + - name: myCertificate # Task Identifier, no relevance in tool run + renewBefore: 31d + request: + csr: service + keyPassword: "newPassword!" + subject: + # Templating needs to go between single quotes to avoid issues when refreshing tokens and saving back + commonName: '{{ Hostname | ToLower -}}.{{- Env "USERDNSDOMAIN" | ToLower }}' + country: US + locality: Salt Lake City + state: Utah + organization: Venafi Inc + orgUnits: + - engineering + - marketing + zone: "Open Source\\vcert" + installations: + - format: PEM + file: "/path/to/my/certificate/cert.cer" + chainFile: "/path/to/my/certificate/chain.cer" + keyFile: "/path/to/my/certificate/key.pem" + afterInstallAction: "echo Success!!!" diff --git a/examples/svc-account/main.go b/examples/tlspc-svc-account/main.go similarity index 100% rename from examples/svc-account/main.go rename to examples/tlspc-svc-account/main.go diff --git a/pkg/endpoint/authentication.go b/pkg/endpoint/authentication.go index 1802ed26..30047fb6 100644 --- a/pkg/endpoint/authentication.go +++ b/pkg/endpoint/authentication.go @@ -33,8 +33,8 @@ type Authentication struct { // API key APIKey string `yaml:"apiKey,omitempty"` // Service account - TenantID string `yaml:"tlspcTenantId,omitempty"` - ExternalIdPJWT string `yaml:"tlspcJWT,omitempty"` + TenantID string `yaml:"tenantId,omitempty"` + ExternalIdPJWT string `yaml:"externalJWT,omitempty"` // IDP Auth method ClientId string `yaml:"clientId,omitempty"` diff --git a/pkg/playbook/app/domain/authentication.go b/pkg/playbook/app/domain/authentication.go index ecd4efed..7885693a 100644 --- a/pkg/playbook/app/domain/authentication.go +++ b/pkg/playbook/app/domain/authentication.go @@ -27,6 +27,8 @@ import ( const ( accessToken = "accessToken" apiKey = "apiKey" + tenantID = "tenantId" + externalJWT = "externalJWT" clientID = "clientId" clientSecret = "clientSecret" refreshToken = "refreshToken" @@ -53,6 +55,12 @@ func (a Authentication) MarshalYAML() (interface{}, error) { if a.APIKey != "" { values[apiKey] = a.APIKey } + if a.TenantID != "" { + values[tenantID] = a.TenantID + } + if a.ExternalIdPJWT != "" { + values[externalJWT] = a.ExternalIdPJWT + } if a.ClientId != "" { values[clientID] = a.ClientId } @@ -66,7 +74,6 @@ func (a Authentication) MarshalYAML() (interface{}, error) { if a.IdentityProvider.Audience != "" { values[idPAudience] = a.IdentityProvider.Audience } - //values[idP] = a.IdentityProvider } if a.RefreshToken != "" { values[refreshToken] = a.RefreshToken @@ -95,6 +102,12 @@ func (a *Authentication) UnmarshalYAML(value *yaml.Node) error { if val, found := authMap[apiKey]; found { a.APIKey = val.(string) } + if val, found := authMap[tenantID]; found { + a.TenantID = val.(string) + } + if val, found := authMap[externalJWT]; found { + a.ExternalIdPJWT = val.(string) + } if val, found := authMap[clientID]; found { a.ClientId = val.(string) } diff --git a/pkg/playbook/app/domain/connection.go b/pkg/playbook/app/domain/connection.go index fc87a4a6..71ccd669 100644 --- a/pkg/playbook/app/domain/connection.go +++ b/pkg/playbook/app/domain/connection.go @@ -105,10 +105,28 @@ func isValidTpp(c Connection) (bool, error) { func isValidVaaS(c Connection) (bool, error) { // Credentials are not empty - if c.Credentials.APIKey == "" { + apikey := false + if c.Credentials.APIKey != "" { + apikey = true + } + + svcaccount := false + if c.Credentials.TenantID != "" { + svcaccount = true + } + + if !apikey && !svcaccount { return false, ErrNoCredentials } + if apikey { + return true, nil + } + + if c.Credentials.ExternalIdPJWT == "" { + return false, ErrNoExternalJWT + } + return true, nil } diff --git a/pkg/playbook/app/domain/error.go b/pkg/playbook/app/domain/error.go index 6d855a21..459c2633 100644 --- a/pkg/playbook/app/domain/error.go +++ b/pkg/playbook/app/domain/error.go @@ -84,4 +84,6 @@ var ( ErrNoClientId = fmt.Errorf("no cliendId defined. Firefly platform requires a clientId to request OAuth2 token") // ErrNoIdentityProviderURL is thrown when platform is Firefly and no config.credentials.tokenURL is defined to request an OAuth2 Token ErrNoIdentityProviderURL = fmt.Errorf("no tokenURL defined in credentials. tokenURL is required to request OAuth2 token") + // ErrNoExternalJWT is thrown when platform is TLSPC/VAAS, a tenantId has been passed but no config.credentials.externalJWT is set + ErrNoExternalJWT = fmt.Errorf("no externalJWT defined in credentials. externalJWT is required to request an access token from TLSPC") ) diff --git a/pkg/playbook/app/vcertutil/helper.go b/pkg/playbook/app/vcertutil/helper.go index bee4db9b..24a30fce 100644 --- a/pkg/playbook/app/vcertutil/helper.go +++ b/pkg/playbook/app/vcertutil/helper.go @@ -43,7 +43,7 @@ const ( // OriginName represents the Origin of the Request set in a Custom Field OriginName = "Venafi VCert Playbook" - userProvidedCSRPrefix = "file:" + filePrefix = "file:" ) func loadTrustBundle(path string) string { @@ -188,8 +188,8 @@ func setCSR(playbookRequest domain.PlaybookRequest, vcertRequest *certificate.Re vcertRequest.CsrOrigin = certificate.LocalGeneratedCSR //CSR is user provided. Load CSR from file - if strings.HasPrefix(playbookRequest.CsrOrigin, userProvidedCSRPrefix) { - file := playbookRequest.CsrOrigin[len(userProvidedCSRPrefix):] + if strings.HasPrefix(playbookRequest.CsrOrigin, filePrefix) { + file := playbookRequest.CsrOrigin[len(filePrefix):] csr, err := readCSRFromFile(file) if err != nil { zap.L().Warn("failed to read CSR from file", zap.String("file", file), zap.Error(err)) @@ -216,7 +216,7 @@ func setCSR(playbookRequest domain.PlaybookRequest, vcertRequest *certificate.Re } func readCSRFromFile(fileName string) ([]byte, error) { - bytes, err := os.ReadFile(fileName) + bytes, err := readFile(fileName) if err != nil { return nil, err } @@ -231,3 +231,11 @@ func readCSRFromFile(fileName string) ([]byte, error) { bytes = rest } } + +func readFile(fileName string) ([]byte, error) { + bytes, err := os.ReadFile(fileName) + if err != nil { + return bytes, err + } + return bytes, nil +} diff --git a/pkg/playbook/app/vcertutil/vcertutil.go b/pkg/playbook/app/vcertutil/vcertutil.go index 9f24b10b..4a4f7a35 100644 --- a/pkg/playbook/app/vcertutil/vcertutil.go +++ b/pkg/playbook/app/vcertutil/vcertutil.go @@ -21,6 +21,7 @@ import ( "crypto/x509/pkix" "errors" "fmt" + "strings" "time" "go.uber.org/zap" @@ -82,26 +83,22 @@ func EnrollCertificate(config domain.Config, request domain.PlaybookRequest) (*c } func buildClient(config domain.Config, zone string) (endpoint.Connector, error) { - vConfig := &vcert.Config{ - ConnectorType: config.Connection.GetConnectorType(), - BaseUrl: config.Connection.URL, - Zone: zone, - Credentials: &endpoint.Authentication{ - APIKey: config.Connection.Credentials.APIKey, - Scope: config.Connection.Credentials.Scope, - ClientId: config.Connection.Credentials.ClientId, - AccessToken: config.Connection.Credentials.AccessToken, - ClientSecret: config.Connection.Credentials.ClientSecret, - }, + vcertConfig := &vcert.Config{ + ConnectorType: config.Connection.GetConnectorType(), + BaseUrl: config.Connection.URL, + Zone: zone, ConnectionTrust: loadTrustBundle(config.Connection.TrustBundlePath), LogVerbose: false, } - if config.Connection.Credentials.IdentityProvider != nil { - vConfig.Credentials.IdentityProvider = config.Connection.Credentials.IdentityProvider + // build Authentication object + vcertAuth, err := buildVCertAuthentication(config.Connection.Credentials) + if err != nil { + return nil, err } + vcertConfig.Credentials = vcertAuth - client, err := vcert.NewClient(vConfig) + client, err := vcert.NewClient(vcertConfig) if err != nil { return nil, err } @@ -109,6 +106,132 @@ func buildClient(config domain.Config, zone string) (endpoint.Connector, error) return client, nil } +func buildVCertAuthentication(playbookAuth domain.Authentication) (*endpoint.Authentication, error) { + offset := len(filePrefix) + attrPrefix := "config.connection.credentials" + + vcertAuth := &endpoint.Authentication{} + + // Cloud API key + apiKey := playbookAuth.APIKey + if strings.HasPrefix(apiKey, filePrefix) { + data, err := readFile(apiKey[offset:]) + if err != nil { + attribute := fmt.Sprintf("%s.apiKey", attrPrefix) + return nil, fmt.Errorf("failed to read value [%s] from authentication attribute: %w", attribute, err) + } + apiKey = strings.TrimSpace(string(data)) + } + vcertAuth.APIKey = apiKey + + // Cloud tenant ID + tenantID := playbookAuth.TenantID + if strings.HasPrefix(tenantID, filePrefix) { + data, err := readFile(tenantID[offset:]) + if err != nil { + attribute := fmt.Sprintf("%s.tenantId", attrPrefix) + return nil, fmt.Errorf("failed to read value [%s] from authentication attribute: %w", attribute, err) + } + tenantID = strings.TrimSpace(string(data)) + } + vcertAuth.TenantID = tenantID + + // Cloud JWT + jwt := playbookAuth.ExternalIdPJWT + if strings.HasPrefix(jwt, filePrefix) { + data, err := readFile(jwt[offset:]) + if err != nil { + attribute := fmt.Sprintf("%s.externalJWT", attrPrefix) + return nil, fmt.Errorf("failed to read value [%s] from authentication attribute: %w", attribute, err) + } + jwt = strings.TrimSpace(string(data)) + } + vcertAuth.ExternalIdPJWT = jwt + + // Access token + accessToken := playbookAuth.AccessToken + if strings.HasPrefix(accessToken, filePrefix) { + data, err := readFile(accessToken[offset:]) + if err != nil { + attribute := fmt.Sprintf("%s.accessToken", attrPrefix) + return nil, fmt.Errorf("failed to read value [%s] from authentication attribute: %w", attribute, err) + } + accessToken = strings.TrimSpace(string(data)) + } + vcertAuth.AccessToken = accessToken + + // Scope + scope := playbookAuth.Scope + if strings.HasPrefix(scope, filePrefix) { + data, err := readFile(scope[offset:]) + if err != nil { + attribute := fmt.Sprintf("%s.scope", attrPrefix) + return nil, fmt.Errorf("failed to read value [%s] from authentication attribute: %w", attribute, err) + } + scope = strings.TrimSpace(string(data)) + } + vcertAuth.Scope = scope + + // Client ID + clientID := playbookAuth.ClientId + if strings.HasPrefix(clientID, filePrefix) { + data, err := readFile(clientID[offset:]) + if err != nil { + attribute := fmt.Sprintf("%s.clientId", attrPrefix) + return nil, fmt.Errorf("failed to read value [%s] from authentication attribute: %w", attribute, err) + } + clientID = strings.TrimSpace(string(data)) + } + vcertAuth.ClientId = clientID + + // Client secret + clientSecret := playbookAuth.ClientSecret + if strings.HasPrefix(clientSecret, filePrefix) { + data, err := readFile(clientSecret[offset:]) + if err != nil { + attribute := fmt.Sprintf("%s.clientSecret", attrPrefix) + return nil, fmt.Errorf("failed to read value [%s] from authentication attribute: %w", attribute, err) + } + clientSecret = strings.TrimSpace(string(data)) + } + vcertAuth.ClientSecret = clientSecret + + // Return here as Identity provider is nil + if playbookAuth.IdentityProvider == nil { + return vcertAuth, nil + } + + idp := &endpoint.OAuthProvider{} + + // OAuth provider token url + tokenURL := playbookAuth.IdentityProvider.TokenURL + if strings.HasPrefix(tokenURL, filePrefix) { + data, err := readFile(tokenURL[offset:]) + if err != nil { + attribute := fmt.Sprintf("%s.idP.tokenURL", attrPrefix) + return nil, fmt.Errorf("failed to read value from attribute: %s:%w", attribute, err) + } + tokenURL = strings.TrimSpace(string(data)) + } + idp.TokenURL = tokenURL + + // OAuth provider audience + audience := playbookAuth.IdentityProvider.Audience + if strings.HasPrefix(audience, filePrefix) { + data, err := readFile(audience[len(filePrefix):]) + if err != nil { + attribute := fmt.Sprintf("%s.idP.audience", attrPrefix) + return nil, fmt.Errorf("failed to read value [%s] from authentication attribute: %w", attribute, err) + } + audience = strings.TrimSpace(string(data)) + } + idp.Audience = audience + + vcertAuth.IdentityProvider = idp + + return vcertAuth, nil +} + func buildRequest(request domain.PlaybookRequest) certificate.Request { vcertRequest := certificate.Request{