Vault-21960: Add docker tests for reloading seal configuration on SIGHUP (#24312)

* reload seals on SIGHUP

* add lock in SetSeals

* move lock

* use stubmaker and change wrapper finalize call

* change finalize logic so that old seals will be finalized after new seals are configured

* add changelog

* run make fmt

* fix fmt

* fix panic when reloading seals errors out

* add sighup tests and separate out docker utilities

* add test case

* fix typo

* remove build tag

* fix imports

* refactoring to make functions more general and avoid conflicts

* add utility funcs

* separate out config copy into function

* fix error message

* fix error messages
This commit is contained in:
Rachel Culpepper 2023-12-12 16:26:00 -05:00 committed by GitHub
parent 879f9c9bfd
commit 9eca3ebde1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 474 additions and 0 deletions

View File

@ -0,0 +1,313 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package seal_binary
import (
"context"
"fmt"
"io"
"net/url"
"os"
"path"
"github.com/docker/docker/api/types"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/api"
dockhelper "github.com/hashicorp/vault/sdk/helper/docker"
)
const (
containerConfig = `
{
"storage": {
"file": {
"path": "/tmp",
}
},
"disable_mlock": true,
"listener": [{
"tcp": {
"address": "0.0.0.0:8200",
"tls_disable": "true"
}
}],
"api_addr": "http://0.0.0.0:8200",
"cluster_addr": "http://0.0.0.0:8201",
%s
}`
sealConfig = `
"seal": [
%s
]
`
transitParameters = `
"address": "%s",
"token": "%s",
"mount_path": "%s",
"key_name": "%s",
"name": "%s"
`
transitStanza = `
{
"transit": {
%s,
"priority": %d,
"disabled": %s
}
}
`
)
type transitContainerConfig struct {
Address string
Token string
MountPaths []string
KeyNames []string
}
func createDockerImage(imageRepo, imageTag, vaultBinary string) error {
runner, err := dockhelper.NewServiceRunner(dockhelper.RunOptions{
ContainerName: "vault",
ImageRepo: imageRepo,
ImageTag: "latest",
})
if err != nil {
return fmt.Errorf("error creating runner: %w", err)
}
f, err := os.Open(vaultBinary)
if err != nil {
return fmt.Errorf("error opening vault binary file: %w", err)
}
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("error reading vault binary file: %w", err)
}
bCtx := dockhelper.NewBuildContext()
bCtx["vault"] = &dockhelper.FileContents{
Data: data,
Mode: 0o755,
}
containerFile := fmt.Sprintf(`
FROM %s:latest
COPY vault /bin/vault
`, imageRepo)
_, err = runner.BuildImage(context.Background(), containerFile, bCtx,
dockhelper.BuildRemove(true), dockhelper.BuildForceRemove(true),
dockhelper.BuildPullParent(true),
dockhelper.BuildTags([]string{fmt.Sprintf("hashicorp/vault:%s", imageTag)}))
if err != nil {
return fmt.Errorf("error building docker image: %w", err)
}
return nil
}
func createContainerWithConfig(config string, imageRepo, imageTag string, logConsumer func(s string)) (*dockhelper.Service, *dockhelper.Runner, error) {
runner, err := dockhelper.NewServiceRunner(dockhelper.RunOptions{
ContainerName: "vault",
ImageRepo: imageRepo,
ImageTag: imageTag,
Cmd: []string{
"server", "-log-level=trace",
},
Ports: []string{"8200/tcp"},
Env: []string{fmt.Sprintf("VAULT_LICENSE=%s", os.Getenv("VAULT_LICENSE")), fmt.Sprintf("VAULT_LOCAL_CONFIG=%s", config)},
LogConsumer: logConsumer,
})
if err != nil {
return nil, nil, fmt.Errorf("error creating runner: %w", err)
}
svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (dockhelper.ServiceConfig, error) {
return *dockhelper.NewServiceURL(url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", host, port)}), nil
})
if err != nil {
return nil, nil, fmt.Errorf("could not start docker vault: %w", err)
}
return svc, runner, nil
}
func createTransitTestContainer(imageRepo, imageTag string, numKeys int) (*dockhelper.Service, *transitContainerConfig, error) {
rootToken, err := uuid.GenerateUUID()
if err != nil {
return nil, nil, fmt.Errorf("error generating UUID: %w", err)
}
mountPaths := make([]string, numKeys)
keyNames := make([]string, numKeys)
for i := range mountPaths {
mountPaths[i], err = uuid.GenerateUUID()
if err != nil {
return nil, nil, fmt.Errorf("error generating UUID: %w", err)
}
keyNames[i], err = uuid.GenerateUUID()
if err != nil {
return nil, nil, fmt.Errorf("error generating UUID: %w", err)
}
}
runner, err := dockhelper.NewServiceRunner(dockhelper.RunOptions{
ContainerName: "vault",
ImageRepo: imageRepo,
ImageTag: imageTag,
Cmd: []string{
"server", "-log-level=trace", "-dev", fmt.Sprintf("-dev-root-token-id=%s", rootToken),
"-dev-listen-address=0.0.0.0:8200",
},
Env: []string{fmt.Sprintf("VAULT_LICENSE=%s", os.Getenv("VAULT_LICENSE"))},
Ports: []string{"8200/tcp"},
})
if err != nil {
return nil, nil, fmt.Errorf("could not create runner: %w", err)
}
svc, err := runner.StartService(context.Background(), func(ctx context.Context, host string, port int) (dockhelper.ServiceConfig, error) {
c := *dockhelper.NewServiceURL(url.URL{Scheme: "http", Host: fmt.Sprintf("%s:%d", host, port)})
clientConfig := api.DefaultConfig()
clientConfig.Address = c.URL().String()
vault, err := api.NewClient(clientConfig)
if err != nil {
return nil, err
}
vault.SetToken(rootToken)
// Set up transit mounts and keys
for i := range mountPaths {
if err := vault.Sys().Mount(mountPaths[i], &api.MountInput{
Type: "transit",
}); err != nil {
return nil, err
}
if _, err := vault.Logical().Write(path.Join(mountPaths[i], "keys", keyNames[i]), map[string]interface{}{}); err != nil {
return nil, err
}
}
return c, nil
})
if err != nil {
return nil, nil, fmt.Errorf("could not start docker vault: %w", err)
}
mapping, err := runner.GetNetworkAndAddresses(svc.Container.Name)
if err != nil {
svc.Cleanup()
return nil, nil, fmt.Errorf("failed to get container network information: %w", err)
}
if len(mapping) != 1 {
svc.Cleanup()
return nil, nil, fmt.Errorf("expected 1 network mapping, got %d", len(mapping))
}
var ip string
for _, ip = range mapping {
// capture the container IP address from the map
}
return svc,
&transitContainerConfig{
Address: fmt.Sprintf("http://%s:8200", ip),
Token: rootToken,
MountPaths: mountPaths,
KeyNames: keyNames,
}, nil
}
func validateVaultStatusAndSealType(client *api.Client, expectedSealType string) error {
statusResp, err := client.Sys().SealStatus()
if err != nil {
return fmt.Errorf("error getting vault status: %w", err)
}
if statusResp.Sealed {
return fmt.Errorf("expected vault to be unsealed, but it is sealed")
}
if statusResp.Type != expectedSealType {
return fmt.Errorf("unexpected seal type: expected %s, got %s", expectedSealType, statusResp.Type)
}
return nil
}
func testClient(address string) (*api.Client, error) {
clientConfig := api.DefaultConfig()
clientConfig.Address = address
testClient, err := api.NewClient(clientConfig)
if err != nil {
return nil, err
}
return testClient, nil
}
func initializeVault(client *api.Client, sealType string) ([]string, string, error) {
var keys []string
var token string
if sealType == "shamir" {
initResp, err := client.Sys().Init(&api.InitRequest{
SecretThreshold: 1,
SecretShares: 1,
})
if err != nil {
return nil, "", err
}
keys = initResp.Keys
token = initResp.RootToken
_, err = client.Sys().Unseal(initResp.Keys[0])
if err != nil {
return nil, "", err
}
} else {
initResp, err := client.Sys().Init(&api.InitRequest{
RecoveryShares: 1,
RecoveryThreshold: 1,
})
if err != nil {
return nil, "", err
}
keys = initResp.RecoveryKeys
token = initResp.RootToken
}
return keys, token, nil
}
func copyConfigToContainer(config, containerID string, runner *dockhelper.Runner) error {
bCtx := dockhelper.NewBuildContext()
bCtx["local.json"] = &dockhelper.FileContents{
Data: []byte(config),
Mode: 0o644,
}
tar, err := bCtx.ToTarball()
if err != nil {
return fmt.Errorf("error creating config tarball: %w", err)
}
err = runner.DockerAPI.CopyToContainer(context.Background(), containerID, "/vault/config", tar, types.CopyToContainerOptions{})
if err != nil {
return fmt.Errorf("error copying config to container: %w", err)
}
return nil
}

View File

@ -0,0 +1,161 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !enterprise
package seal_binary
import (
"context"
"fmt"
"os"
"testing"
"time"
)
func TestSealReloadSIGHUP(t *testing.T) {
binary := os.Getenv("VAULT_BINARY")
if binary == "" {
t.Skip("only running docker test with $VAULT_BINARY present")
}
transitContainer, transitConfig, err := createTransitTestContainer("hashicorp/vault", "latest", 2)
if err != nil {
t.Fatalf("error creating vault container: %s", err)
}
defer transitContainer.Cleanup()
firstTransitKeyConfig := fmt.Sprintf(transitParameters,
transitConfig.Address,
transitConfig.Token,
transitConfig.MountPaths[0],
transitConfig.KeyNames[0],
"transit-seal-1",
)
secondTransitKeyConfig := fmt.Sprintf(transitParameters,
transitConfig.Address,
transitConfig.Token,
transitConfig.MountPaths[1],
transitConfig.KeyNames[1],
"transit-seal-2",
)
testCases := map[string]struct {
sealStanzas []string
expectedSealTypes []string
}{
"migrate transit to transit": {
sealStanzas: []string{
fmt.Sprintf(transitStanza, firstTransitKeyConfig, 1, "false"),
fmt.Sprintf(transitStanza, firstTransitKeyConfig, 2, "true") + "," +
fmt.Sprintf(transitStanza, secondTransitKeyConfig, 1, "false"),
fmt.Sprintf(transitStanza, secondTransitKeyConfig, 1, "false"),
},
expectedSealTypes: []string{
"transit",
"transit",
"transit",
},
},
"migrate shamir to transit fails": {
sealStanzas: []string{
"",
fmt.Sprintf(transitStanza, firstTransitKeyConfig, 1, "false"),
},
expectedSealTypes: []string{
"shamir",
"shamir",
},
},
"migrate transit to shamir fails": {
sealStanzas: []string{
fmt.Sprintf(transitStanza, firstTransitKeyConfig, 1, "false"),
"",
},
expectedSealTypes: []string{
"transit",
"transit",
},
},
"replacing seal fails": {
sealStanzas: []string{
fmt.Sprintf(transitStanza, firstTransitKeyConfig, 1, "false"),
fmt.Sprintf(transitStanza, secondTransitKeyConfig, 1, "false"),
},
expectedSealTypes: []string{
"transit",
"transit",
},
},
"more than one seal fails": {
sealStanzas: []string{
fmt.Sprintf(transitStanza, firstTransitKeyConfig, 1, "false"),
fmt.Sprintf(transitStanza, firstTransitKeyConfig, 1, "false") + "," +
fmt.Sprintf(transitStanza, secondTransitKeyConfig, 2, "false"),
},
expectedSealTypes: []string{
"transit",
"transit",
},
},
}
err = createDockerImage("hashicorp/vault", "test-image", os.Getenv("VAULT_BINARY"))
if err != nil {
t.Fatalf("error creating docker image: %s", err)
}
for name, test := range testCases {
t.Run(name, func(t *testing.T) {
var sealList string
if test.sealStanzas[0] != "" {
sealList = fmt.Sprintf(sealConfig, test.sealStanzas[0])
}
vaultConfig := fmt.Sprintf(containerConfig, sealList)
svc, runner, err := createContainerWithConfig(vaultConfig, "hashicorp/vault", "test-image", func(s string) { t.Log(s) })
defer svc.Cleanup()
if err != nil {
t.Fatalf("error creating container: %s", err)
}
time.Sleep(5 * time.Second)
client, err := testClient(svc.Config.URL().String())
if err != nil {
t.Fatalf("err: %s", err)
}
_, token, err := initializeVault(client, test.expectedSealTypes[0])
if err != nil {
t.Fatalf("error initializing vault: %s", err)
}
client.SetToken(token)
for i := range test.sealStanzas {
if test.sealStanzas[i] != "" {
sealList = fmt.Sprintf(sealList, test.sealStanzas[i])
}
vaultConfig = fmt.Sprintf(containerConfig, sealList)
err = copyConfigToContainer(vaultConfig, svc.Container.ID, runner)
if err != nil {
t.Fatalf("error copying over config file: %s", err)
}
err = runner.DockerAPI.ContainerKill(context.Background(), svc.Container.ID, "SIGHUP")
if err != nil {
t.Fatalf("error sending SIGHUP: %s", err)
}
err = validateVaultStatusAndSealType(client, test.expectedSealTypes[i])
if err != nil {
t.Fatalf("seal type check failed: %s", err)
}
}
})
}
}