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

Backport of keyring: warn if removing a key that was used for encrypting variables into release/1.9.x #24794

Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .changelog/24766.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
keyring: Warn if deleting a key previously used to encrypt an existing variable
```
8 changes: 6 additions & 2 deletions api/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package api
import (
"fmt"
"net/url"
"strconv"
)

// Keyring is used to access the Variables keyring.
Expand Down Expand Up @@ -60,14 +61,17 @@ func (k *Keyring) List(q *QueryOptions) ([]*RootKeyMeta, *QueryMeta, error) {

// Delete deletes a specific inactive key from the keyring
func (k *Keyring) Delete(opts *KeyringDeleteOptions, w *WriteOptions) (*WriteMeta, error) {
wm, err := k.client.delete(fmt.Sprintf("/v1/operator/keyring/key/%v",
url.PathEscape(opts.KeyID)), nil, nil, w)
wm, err := k.client.delete(fmt.Sprintf("/v1/operator/keyring/key/%v?force=%v",
url.PathEscape(opts.KeyID), strconv.FormatBool(opts.Force)), nil, nil, w)
return wm, err
}

// KeyringDeleteOptions are parameters for the Delete API
type KeyringDeleteOptions struct {
KeyID string // UUID
// Force can be used to force deletion of a root keyring that was used to encrypt
// an existing variable or to sign a workload identity
Force bool
}

// Rotate requests a key rotation
Expand Down
4 changes: 2 additions & 2 deletions api/keyring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ func TestKeyring_CRUD(t *testing.T) {
assertQueryMeta(t, qm)
must.Len(t, 2, keys)

// Delete the old key
wm, err = kr.Delete(&KeyringDeleteOptions{KeyID: oldKeyID}, nil)
// Delete the old key with force
wm, err = kr.Delete(&KeyringDeleteOptions{KeyID: oldKeyID, Force: true}, nil)
must.NoError(t, err)
assertWriteMeta(t, wm)

Expand Down
17 changes: 14 additions & 3 deletions command/agent/keyring_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,20 @@ func (s *HTTPServer) KeyringRequest(resp http.ResponseWriter, req *http.Request)
return s.keyringListRequest(resp, req)
case strings.HasPrefix(path, "key"):
keyID := strings.TrimPrefix(req.URL.Path, "/v1/operator/keyring/key/")

var forceBool bool
var err error
forceQuery, ok := req.URL.Query()["force"]
if ok {
forceBool, err = strconv.ParseBool(forceQuery[0])
}

if err != nil {
return nil, CodedError(422, "invalid force parameter")
}
switch req.Method {
case http.MethodDelete:
return s.keyringDeleteRequest(resp, req, keyID)
return s.keyringDeleteRequest(resp, req, keyID, forceBool)
default:
return nil, CodedError(405, ErrInvalidMethod)
}
Expand Down Expand Up @@ -185,9 +196,9 @@ func (s *HTTPServer) keyringRotateRequest(resp http.ResponseWriter, req *http.Re
return out, nil
}

func (s *HTTPServer) keyringDeleteRequest(resp http.ResponseWriter, req *http.Request, keyID string) (interface{}, error) {
func (s *HTTPServer) keyringDeleteRequest(resp http.ResponseWriter, req *http.Request, keyID string, force bool) (interface{}, error) {

args := structs.KeyringDeleteRootKeyRequest{KeyID: keyID}
args := structs.KeyringDeleteRootKeyRequest{KeyID: keyID, Force: force}
s.parseWriteRequest(req, &args.WriteRequest)

var out structs.KeyringDeleteRootKeyResponse
Expand Down
14 changes: 14 additions & 0 deletions command/agent/keyring_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/go-jose/go-jose/v3"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/shoenig/test/must"

"github.com/hashicorp/nomad/ci"
Expand All @@ -36,6 +37,13 @@ func TestHTTP_Keyring_CRUD(t *testing.T) {
must.Len(t, 1, listResp)
key0 := listResp[0].KeyID

// Create a variable to test force key deletion
state := s.server.State()
encryptedVar := mock.VariableEncrypted()
encryptedVar.KeyID = key0
varSetResp := state.VarSet(0, &structs.VarApplyStateRequest{Var: encryptedVar})
must.NoError(t, varSetResp.Error)

// Rotate

req, err = http.NewRequest(http.MethodPut, "/v1/operator/keyring/rotate", nil)
Expand Down Expand Up @@ -87,6 +95,12 @@ func TestHTTP_Keyring_CRUD(t *testing.T) {
req, err = http.NewRequest(http.MethodDelete, "/v1/operator/keyring/key/"+key0, nil)
must.NoError(t, err)
obj, err = s.Server.KeyringRequest(respW, req)
must.Error(t, err)
must.EqError(t, err, "root key in use, cannot delete")

req, err = http.NewRequest(http.MethodDelete, "/v1/operator/keyring/key/"+key0+"?force=true", nil)
must.NoError(t, err)
obj, err = s.Server.KeyringRequest(respW, req)
must.NoError(t, err)

req, err = http.NewRequest(http.MethodGet, "/v1/operator/keyring/keys", nil)
Expand Down
13 changes: 11 additions & 2 deletions command/operator_root_keyring_remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ Usage: nomad operator root keyring remove [options] <key ID>

General Options:

` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace)
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `

Remove Options:

-force
Remove the key even if it was used to sign an existing variable
or workload identity.
`

return strings.TrimSpace(helpText)
}
Expand All @@ -51,11 +58,12 @@ func (c *OperatorRootKeyringRemoveCommand) Name() string {
}

func (c *OperatorRootKeyringRemoveCommand) Run(args []string) int {
var verbose bool
var force, verbose bool

flags := c.Meta.FlagSet("root keyring remove", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&verbose, "verbose", false, "")
flags.BoolVar(&force, "force", false, "Forces deletion of the root keyring even if it's in use.")

if err := flags.Parse(args); err != nil {
return 1
Expand All @@ -76,6 +84,7 @@ func (c *OperatorRootKeyringRemoveCommand) Run(args []string) int {
}
_, err = client.Keyring().Delete(&api.KeyringDeleteOptions{
KeyID: removeKey,
Force: force,
}, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
Expand Down
15 changes: 15 additions & 0 deletions nomad/keyring_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package nomad

import (
"errors"
"fmt"
"time"

Expand Down Expand Up @@ -344,10 +345,24 @@ func (k *Keyring) Delete(args *structs.KeyringDeleteRootKeyRequest, reply *struc
if err != nil {
return err
}

if rootKey == nil {
return errors.New("root key not found")
}

if rootKey != nil && rootKey.IsActive() {
return fmt.Errorf("active root key cannot be deleted - call rotate first")
}

// make sure the key was used to encrypt an existing variable
rootKeyInUse, err := snap.IsRootKeyInUse(args.KeyID)
if err != nil {
return err
}
if rootKeyInUse && !args.Force {
return errors.New("root key in use, cannot delete")
}

_, index, err = k.srv.raftApply(structs.WrappedRootKeysDeleteRequestType, args)
if err != nil {
return err
Expand Down
65 changes: 38 additions & 27 deletions nomad/keyring_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import (
"time"

msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc/v2"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/require"

"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
"github.com/shoenig/test/must"
)

// TestKeyringEndpoint_CRUD exercises the basic keyring operations
Expand All @@ -26,11 +25,11 @@ func TestKeyringEndpoint_CRUD(t *testing.T) {
defer shutdown()
testutil.WaitForKeyring(t, srv.RPC, "global")
codec := rpcClient(t, srv)
state := srv.fsm.State()

// Upsert a new key

key, err := structs.NewUnwrappedRootKey(structs.EncryptionAlgorithmAES256GCM)
require.NoError(t, err)
must.NoError(t, err)
id := key.Meta.KeyID
key = key.MakeActive()

Expand All @@ -41,12 +40,19 @@ func TestKeyringEndpoint_CRUD(t *testing.T) {
var updateResp structs.KeyringUpdateRootKeyResponse

err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
must.EqError(t, err, structs.ErrPermissionDenied.Error())

updateReq.AuthToken = rootToken.SecretID
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.NoError(t, err)
require.NotEqual(t, uint64(0), updateResp.Index)
must.NoError(t, err)
must.NotEq(t, uint64(0), updateResp.Index)

// Upsert a variable and encrypt it with that key (used for key deletion test
// below)
encryptedVar := mock.VariableEncrypted()
encryptedVar.KeyID = key.Meta.KeyID
varSetResp := state.VarSet(0, &structs.VarApplyStateRequest{Var: encryptedVar})
must.NoError(t, varSetResp.Error)

// Get doesn't need a token here because it uses mTLS role verification
getReq := &structs.KeyringGetRootKeyRequest{
Expand All @@ -56,9 +62,9 @@ func TestKeyringEndpoint_CRUD(t *testing.T) {
var getResp structs.KeyringGetRootKeyResponse

err = msgpackrpc.CallWithCodec(codec, "Keyring.Get", getReq, &getResp)
require.NoError(t, err)
require.Equal(t, updateResp.Index, getResp.Index)
require.Equal(t, structs.EncryptionAlgorithmAES256GCM, getResp.Key.Meta.Algorithm)
must.NoError(t, err)
must.Eq(t, updateResp.Index, getResp.Index)
must.Eq(t, structs.EncryptionAlgorithmAES256GCM, getResp.Key.Meta.Algorithm)

// Make a blocking query for List and wait for an Update. Note
// that Get queries don't need ACL tokens in the test server
Expand All @@ -82,13 +88,13 @@ func TestKeyringEndpoint_CRUD(t *testing.T) {

updateReq.RootKey.Meta.CreateTime = time.Now().UTC().UnixNano()
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.NoError(t, err)
require.NotEqual(t, uint64(0), updateResp.Index)
must.NoError(t, err)
must.NotEq(t, uint64(0), updateResp.Index)

// wait for the blocking query to complete and check the response
require.NoError(t, <-errCh)
require.Equal(t, listResp.Index, updateResp.Index)
require.Len(t, listResp.Keys, 2) // bootstrap + new one
must.NoError(t, <-errCh)
must.Eq(t, listResp.Index, updateResp.Index)
must.SliceLen(t, 2, listResp.Keys) /// bootstrap + new one

// Delete the key and verify that it's gone

Expand All @@ -99,20 +105,25 @@ func TestKeyringEndpoint_CRUD(t *testing.T) {
var delResp structs.KeyringDeleteRootKeyResponse

err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
must.EqError(t, err, structs.ErrPermissionDenied.Error())

delReq.AuthToken = rootToken.SecretID
err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
require.EqualError(t, err, "active root key cannot be deleted - call rotate first")
must.EqError(t, err, "active root key cannot be deleted - call rotate first")

// set inactive
updateReq.RootKey = updateReq.RootKey.MakeInactive()
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.NoError(t, err)
must.NoError(t, err)

err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
require.NoError(t, err)
require.Greater(t, delResp.Index, getResp.Index)
must.EqError(t, err, "root key in use, cannot delete")

// delete with force
delReq.Force = true
err = msgpackrpc.CallWithCodec(codec, "Keyring.Delete", delReq, &delResp)
must.NoError(t, err)
must.Greater(t, getResp.Index, delResp.Index)

listReq := &structs.KeyringListRootKeyMetaRequest{
QueryOptions: structs.QueryOptions{
Expand All @@ -121,9 +132,9 @@ func TestKeyringEndpoint_CRUD(t *testing.T) {
},
}
err = msgpackrpc.CallWithCodec(codec, "Keyring.List", listReq, &listResp)
require.NoError(t, err)
require.Greater(t, listResp.Index, getResp.Index)
require.Len(t, listResp.Keys, 1) // just the bootstrap key
must.NoError(t, err)
must.Greater(t, getResp.Index, listResp.Index)
must.SliceLen(t, 1, listResp.Keys) // just the bootstrap key
}

// TestKeyringEndpoint_validateUpdate exercises all the various
Expand All @@ -140,7 +151,7 @@ func TestKeyringEndpoint_InvalidUpdates(t *testing.T) {

// Setup an existing key
key, err := structs.NewUnwrappedRootKey(structs.EncryptionAlgorithmAES256GCM)
require.NoError(t, err)
must.NoError(t, err)
id := key.Meta.KeyID
key = key.MakeActive()

Expand All @@ -153,7 +164,7 @@ func TestKeyringEndpoint_InvalidUpdates(t *testing.T) {
}
var updateResp structs.KeyringUpdateRootKeyResponse
err = msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.NoError(t, err)
must.NoError(t, err)

testCases := []struct {
key *structs.UnwrappedRootKey
Expand Down Expand Up @@ -211,7 +222,7 @@ func TestKeyringEndpoint_InvalidUpdates(t *testing.T) {
}
var updateResp structs.KeyringUpdateRootKeyResponse
err := msgpackrpc.CallWithCodec(codec, "Keyring.Update", updateReq, &updateResp)
require.EqualError(t, err, tc.expectedErrMsg)
must.EqError(t, err, tc.expectedErrMsg)
})
}

Expand Down
1 change: 1 addition & 0 deletions nomad/structs/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ type KeyringUpdateRootKeyMetaResponse struct {

type KeyringDeleteRootKeyRequest struct {
KeyID string
Force bool
WriteRequest
}

Expand Down
5 changes: 5 additions & 0 deletions website/content/api-docs/operator/keyring.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ The table below shows this endpoint's support for [blocking queries] and
|------------------|--------------|
| `NO` | `management` |

### Parameters

- `force` `(bool: false)` - Remove the key even if it was used to sign an existing variable
or workload identity.

### Sample Request

```shell-session
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ nomad operator root keyring remove [options] <key ID>

@include 'general_options.mdx'

## Remove Options

- `-force`: Remove the key even if it was used to sign an existing variable
or workload identity.


## Examples

```shell-session
Expand Down
Loading