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:
parent
f9617f5315
commit
e3935ca088
@ -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)
|
||||
}
|
||||
|
@ -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
2
go.mod
@ -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
39
pkg/github/secret.go
Normal 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
30
pkg/github/secret_test.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user