Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLSPC service account support in VCert Playbooks #442

Merged
merged 3 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
4 changes: 2 additions & 2 deletions pkg/endpoint/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
15 changes: 14 additions & 1 deletion pkg/playbook/app/domain/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
const (
accessToken = "accessToken"
apiKey = "apiKey"
tenantID = "tenantId"
externalJWT = "externalJWT"
clientID = "clientId"
clientSecret = "clientSecret"
refreshToken = "refreshToken"
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
20 changes: 19 additions & 1 deletion pkg/playbook/app/domain/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/playbook/app/domain/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
16 changes: 12 additions & 4 deletions pkg/playbook/app/vcertutil/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand All @@ -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
}
Expand All @@ -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
}
151 changes: 137 additions & 14 deletions pkg/playbook/app/vcertutil/vcertutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"crypto/x509/pkix"
"errors"
"fmt"
"strings"
"time"

"go.uber.org/zap"
Expand Down Expand Up @@ -82,33 +83,155 @@ 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
}

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{
Expand Down