1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-04 04:07:16 +02:00

feat(vault): Vault secret rotation for GH Actions (#4280)

* rotate Vault secret on GH Actions

* test alternative sodium package

* try doing it without libsodium

* disable validity check for testing purposes

* basic unit test

* re-enable secret validity check

* tidy

* tidy parameters

* forgot to update param names in code

* apply review feedback

* improve error logging

* update step metadata

* apply metadata suggestion from review

Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com>

* align githubToken param

* Fix secretStore

* Add alias for githubToken

* Move logic to separate file

---------

Co-authored-by: I557621 <jordi.van.liempt@sap.com>
Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com>
Co-authored-by: Vyacheslav Starostin <vyacheslav.starostin@sap.com>
This commit is contained in:
Jordi van Liempt 2023-04-17 08:35:13 +02:00 committed by GitHub
parent f9617f5315
commit e3935ca088
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 215 additions and 4 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/vault/api"
"github.com/SAP/jenkins-library/pkg/ado"
"github.com/SAP/jenkins-library/pkg/github"
"github.com/SAP/jenkins-library/pkg/jenkins"
"github.com/SAP/jenkins-library/pkg/vault"
@ -131,6 +132,33 @@ func writeVaultSecretIDToStore(config *vaultRotateSecretIdOptions, secretID stri
log.Entry().Warn("Could not write secret ID back to Azure DevOps")
return err
}
case "github":
// Additional info:
// https://github.com/google/go-github/blob/master/example/newreposecretwithxcrypto/main.go
ctx, client, err := github.NewClient(config.GithubToken, config.GithubAPIURL, "", []string{})
if err != nil {
log.Entry().Warnf("Could not write secret ID back to GitHub Actions: GitHub client not created: %v", err)
return err
}
publicKey, _, err := client.Actions.GetRepoPublicKey(ctx, config.Owner, config.Repository)
if err != nil {
log.Entry().Warnf("Could not write secret ID back to GitHub Actions: repository's public key not retrieved: %v", err)
return err
}
encryptedSecret, err := github.CreateEncryptedSecret(config.VaultAppRoleSecretTokenCredentialsID, secretID, publicKey)
if err != nil {
log.Entry().Warnf("Could not write secret ID back to GitHub Actions: secret encryption failed: %v", err)
return err
}
_, err = client.Actions.CreateOrUpdateRepoSecret(ctx, config.Owner, config.Repository, encryptedSecret)
if err != nil {
log.Entry().Warnf("Could not write secret ID back to GitHub Actions: submission to GitHub failed: %v", err)
return err
}
default:
return fmt.Errorf("error: invalid secret store: %s", config.SecretStore)
}

View File

@ -16,7 +16,7 @@ import (
)
type vaultRotateSecretIdOptions struct {
SecretStore string `json:"secretStore,omitempty" validate:"possible-values=jenkins ado"`
SecretStore string `json:"secretStore,omitempty" validate:"possible-values=jenkins ado github"`
JenkinsURL string `json:"jenkinsUrl,omitempty"`
JenkinsCredentialDomain string `json:"jenkinsCredentialDomain,omitempty"`
JenkinsUsername string `json:"jenkinsUsername,omitempty"`
@ -29,6 +29,10 @@ type vaultRotateSecretIdOptions struct {
AdoPersonalAccessToken string `json:"adoPersonalAccessToken,omitempty" validate:"required_if=SecretStore ado"`
AdoProject string `json:"adoProject,omitempty"`
AdoPipelineID int `json:"adoPipelineId,omitempty"`
GithubToken string `json:"githubToken,omitempty" validate:"required_if=SecretStore github"`
GithubAPIURL string `json:"githubApiUrl,omitempty"`
Owner string `json:"owner,omitempty"`
Repository string `json:"repository,omitempty"`
}
// VaultRotateSecretIdCommand Rotate Vault AppRole Secret ID
@ -66,6 +70,7 @@ func VaultRotateSecretIdCommand() *cobra.Command {
log.RegisterSecret(stepConfig.JenkinsUsername)
log.RegisterSecret(stepConfig.JenkinsToken)
log.RegisterSecret(stepConfig.AdoPersonalAccessToken)
log.RegisterSecret(stepConfig.GithubToken)
if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID)
@ -133,7 +138,7 @@ func addVaultRotateSecretIdFlags(cmd *cobra.Command, stepConfig *vaultRotateSecr
cmd.Flags().StringVar(&stepConfig.JenkinsCredentialDomain, "jenkinsCredentialDomain", `_`, "The jenkins credential domain which should be used")
cmd.Flags().StringVar(&stepConfig.JenkinsUsername, "jenkinsUsername", os.Getenv("PIPER_jenkinsUsername"), "The jenkins username")
cmd.Flags().StringVar(&stepConfig.JenkinsToken, "jenkinsToken", os.Getenv("PIPER_jenkinsToken"), "The jenkins token")
cmd.Flags().StringVar(&stepConfig.VaultAppRoleSecretTokenCredentialsID, "vaultAppRoleSecretTokenCredentialsId", os.Getenv("PIPER_vaultAppRoleSecretTokenCredentialsId"), "The Jenkins credential ID or Azure DevOps variable name for the Vault AppRole Secret ID credential")
cmd.Flags().StringVar(&stepConfig.VaultAppRoleSecretTokenCredentialsID, "vaultAppRoleSecretTokenCredentialsId", os.Getenv("PIPER_vaultAppRoleSecretTokenCredentialsId"), "The Jenkins credential ID, Azure DevOps variable name, or GitHub Actions secret name for the Vault AppRole Secret ID credential")
cmd.Flags().StringVar(&stepConfig.VaultServerURL, "vaultServerUrl", os.Getenv("PIPER_vaultServerUrl"), "The URL for the Vault server to use")
cmd.Flags().StringVar(&stepConfig.VaultNamespace, "vaultNamespace", os.Getenv("PIPER_vaultNamespace"), "The Vault namespace that should be used (optional)")
cmd.Flags().IntVar(&stepConfig.DaysBeforeExpiry, "daysBeforeExpiry", 15, "The amount of days before expiry until the secret ID gets rotated")
@ -141,6 +146,10 @@ func addVaultRotateSecretIdFlags(cmd *cobra.Command, stepConfig *vaultRotateSecr
cmd.Flags().StringVar(&stepConfig.AdoPersonalAccessToken, "adoPersonalAccessToken", os.Getenv("PIPER_adoPersonalAccessToken"), "The Azure DevOps personal access token")
cmd.Flags().StringVar(&stepConfig.AdoProject, "adoProject", os.Getenv("PIPER_adoProject"), "The Azure DevOps project ID. Project name also can be used")
cmd.Flags().IntVar(&stepConfig.AdoPipelineID, "adoPipelineId", 0, "The Azure DevOps pipeline ID. Also called as definition ID")
cmd.Flags().StringVar(&stepConfig.GithubToken, "githubToken", os.Getenv("PIPER_githubToken"), "GitHub personal access token as per https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line with the scope 'repo'")
cmd.Flags().StringVar(&stepConfig.GithubAPIURL, "githubApiUrl", `https://api.github.com`, "Set the GitHub API URL that corresponds to the pipeline repository")
cmd.Flags().StringVar(&stepConfig.Owner, "owner", os.Getenv("PIPER_owner"), "Owner of the pipeline GitHub repository")
cmd.Flags().StringVar(&stepConfig.Repository, "repository", os.Getenv("PIPER_repository"), "Name of the pipeline GitHub repository")
cmd.MarkFlagRequired("vaultAppRoleSecretTokenCredentialsId")
cmd.MarkFlagRequired("vaultServerUrl")
@ -298,6 +307,58 @@ func vaultRotateSecretIdMetadata() config.StepData {
Aliases: []config.Alias{},
Default: 0,
},
{
Name: "githubToken",
ResourceRef: []config.ResourceReference{
{
Name: "githubVaultSecretName",
Type: "vaultSecret",
Default: "github",
},
},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{{Name: "access_token"}, {Name: "token"}},
Default: os.Getenv("PIPER_githubToken"),
},
{
Name: "githubApiUrl",
ResourceRef: []config.ResourceReference{},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: `https://api.github.com`,
},
{
Name: "owner",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "github/owner",
},
},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_owner"),
},
{
Name: "repository",
ResourceRef: []config.ResourceReference{
{
Name: "commonPipelineEnvironment",
Param: "github/repository",
},
},
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
Type: "string",
Mandatory: false,
Aliases: []config.Alias{},
Default: os.Getenv("PIPER_repository"),
},
},
},
},

2
go.mod
View File

@ -303,7 +303,7 @@ require (
go.opencensus.io v0.23.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/net v0.7.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
golang.org/x/sys v0.5.0 // indirect

39
pkg/github/secret.go Normal file
View File

@ -0,0 +1,39 @@
package github
import (
"crypto/rand"
"encoding/base64"
"github.com/google/go-github/v45/github"
"golang.org/x/crypto/nacl/box"
"github.com/SAP/jenkins-library/pkg/log"
)
// CreateEncryptedSecret creates an encrypted secret using a public key from a GitHub repository, which can be sent through the GitHub API
// https://github.com/google/go-github/blob/master/example/newreposecretwithxcrypto/main.go
func CreateEncryptedSecret(secretName, secretValue string, publicKey *github.PublicKey) (*github.EncryptedSecret, error) {
decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey.GetKey())
if err != nil {
log.Entry().Warn("Could not decode public key from base64")
return nil, err
}
var boxKey [32]byte
copy(boxKey[:], decodedPublicKey)
secretBytes := []byte(secretValue)
encryptedSecretBytes, err := box.SealAnonymous([]byte{}, secretBytes, &boxKey, rand.Reader)
if err != nil {
log.Entry().Warn("Could not encrypt secret using public key")
return nil, err
}
encryptedSecretString := base64.StdEncoding.EncodeToString(encryptedSecretBytes)
githubSecret := &github.EncryptedSecret{
Name: secretName,
KeyID: publicKey.GetKeyID(),
EncryptedValue: encryptedSecretString,
}
return githubSecret, nil
}

30
pkg/github/secret_test.go Normal file
View File

@ -0,0 +1,30 @@
package github
import (
"encoding/base64"
"testing"
"github.com/google/go-github/v45/github"
"github.com/stretchr/testify/assert"
)
func TestRunGithubCreateEncryptedSecret(t *testing.T) {
t.Parallel()
t.Run("Success", func(t *testing.T) {
mockKeyID := "1"
mockB64Key := base64.StdEncoding.EncodeToString([]byte("testPublicKey"))
mockPubKey := github.PublicKey{KeyID: &mockKeyID, Key: &mockB64Key}
mockName := "testSecret"
mockValue := "testValue"
// test
githubSecret, err := CreateEncryptedSecret(mockName, mockValue, &mockPubKey)
// assert
assert.NoError(t, err)
assert.Equal(t, mockName, githubSecret.Name)
assert.Equal(t, mockKeyID, githubSecret.KeyID)
})
}

View File

@ -16,6 +16,7 @@ spec:
possibleValues:
- jenkins
- ado
- github
- name: jenkinsUrl
type: string
description: "The jenkins url"
@ -68,7 +69,7 @@ spec:
default: jenkins
- name: vaultAppRoleSecretTokenCredentialsId
type: string
description: The Jenkins credential ID or Azure DevOps variable name for the Vault AppRole Secret ID credential
description: The Jenkins credential ID, Azure DevOps variable name, or GitHub Actions secret name for the Vault AppRole Secret ID credential
scope:
- GENERAL
- PARAMETERS
@ -139,3 +140,55 @@ spec:
- STAGES
- STEPS
description: The Azure DevOps pipeline ID. Also called as definition ID
- name: githubToken
aliases:
- name: access_token
- name: token
type: string
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
description: "GitHub personal access token as per
https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line
with the scope 'repo'"
secret: true
mandatoryIf:
- name: secretStore
value: github
resourceRef:
- type: vaultSecret
default: github
name: githubVaultSecretName
- name: githubApiUrl
description: Set the GitHub API URL that corresponds to the pipeline repository
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
type: string
default: "https://api.github.com"
- name: owner
description: Owner of the pipeline GitHub repository
resourceRef:
- name: commonPipelineEnvironment
param: github/owner
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
type: string
- name: repository
description: Name of the pipeline GitHub repository
resourceRef:
- name: commonPipelineEnvironment
param: github/repository
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
type: string