mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-11 15:26:27 +02:00
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:
parent
879f9c9bfd
commit
9eca3ebde1
313
vault/external_tests/seal_binary/seal_docker_util.go
Normal file
313
vault/external_tests/seal_binary/seal_docker_util.go
Normal 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
|
||||
}
|
||||
161
vault/external_tests/seal_binary/seal_sighup_test.go
Normal file
161
vault/external_tests/seal_binary/seal_sighup_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user