-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
386 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package api | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"time" | ||
) | ||
|
||
// Keyring is used to access the Secure Variables keyring | ||
type Keyring struct { | ||
client *Client | ||
} | ||
|
||
// Keyring returns a handle to the Keyring endpoint | ||
func (c *Client) Keyring() *Keyring { | ||
return &Keyring{client: c} | ||
} | ||
|
||
// EncryptionAlgorithm chooses which algorithm is used for | ||
// encrypting / decrypting entries with this key | ||
type EncryptionAlgorithm string | ||
|
||
const ( | ||
EncryptionAlgorithmXChaCha20 EncryptionAlgorithm = "xchacha20" | ||
EncryptionAlgorithmAES256GCM EncryptionAlgorithm = "aes256-gcm" | ||
) | ||
|
||
// RootKey wraps key metadata and the key itself. The key must be | ||
// base64 encoded | ||
type RootKey struct { | ||
Meta *RootKeyMeta | ||
Key string | ||
} | ||
|
||
// RootKeyMeta is the metadata used to refer to a RootKey. | ||
type RootKeyMeta struct { | ||
Active bool | ||
KeyID string // UUID | ||
Algorithm EncryptionAlgorithm | ||
EncryptionsCount uint64 | ||
CreateTime time.Time | ||
CreateIndex uint64 | ||
ModifyIndex uint64 | ||
} | ||
|
||
// List lists all the keyring metadata | ||
func (k *Keyring) List(q *QueryOptions) ([]*RootKeyMeta, *QueryMeta, error) { | ||
var resp []*RootKeyMeta | ||
qm, err := k.client.query("/v1/operator/keyring/keys", &resp, q) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
return resp, qm, nil | ||
} | ||
|
||
// 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, w) | ||
return wm, err | ||
} | ||
|
||
// KeyringDeleteOptions are parameters for the Delete API | ||
type KeyringDeleteOptions struct { | ||
KeyID string // UUID | ||
} | ||
|
||
// Update upserts a key into the keyring | ||
func (k *Keyring) Update(key *RootKey, w *WriteOptions) (*WriteMeta, error) { | ||
wm, err := k.client.write("/v1/operator/keyring/keys", key, nil, w) | ||
return wm, err | ||
} | ||
|
||
// Rotate requests a key rotation | ||
func (k *Keyring) Rotate(opts *KeyringRotateOptions, w *WriteOptions) (*RootKeyMeta, *WriteMeta, error) { | ||
qp := url.Values{} | ||
if opts != nil { | ||
if opts.Algorithm != "" { | ||
qp.Set("algo", string(opts.Algorithm)) | ||
} | ||
if opts.Full { | ||
qp.Set("full", "true") | ||
} | ||
} | ||
resp := &RootKeyMeta{} | ||
wm, err := k.client.write("/v1/operator/keyring/rotate?"+qp.Encode(), nil, resp, w) | ||
return resp, wm, err | ||
} | ||
|
||
// KeyringRotateOptions are parameters for the Rotate API | ||
type KeyringRotateOptions struct { | ||
Full bool | ||
Algorithm EncryptionAlgorithm | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package api | ||
|
||
import ( | ||
"encoding/base64" | ||
"math/rand" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/hashicorp/nomad/api/internal/testutil" | ||
) | ||
|
||
func TestKeyring_CRUD(t *testing.T) { | ||
testutil.Parallel(t) | ||
c, s := makeClient(t, nil, nil) | ||
defer s.Stop() | ||
|
||
kr := c.Keyring() | ||
|
||
// Create a key by requesting a rotation | ||
key, wm, err := kr.Rotate(nil, nil) | ||
require.NoError(t, err) | ||
require.NotNil(t, key) | ||
assertWriteMeta(t, wm) | ||
|
||
// Read all the keys | ||
keys, qm, err := kr.List(&QueryOptions{WaitIndex: key.CreateIndex}) | ||
require.NoError(t, err) | ||
assertQueryMeta(t, qm) | ||
// TODO: there'll be 2 keys here once we get bootstrapping done | ||
require.Len(t, keys, 1) | ||
|
||
// Write a new active key, forcing a rotation | ||
id := "fd77c376-9785-4c80-8e62-4ec3ab5f8b9a" | ||
buf := make([]byte, 128) | ||
rand.Read(buf) | ||
encodedKey := make([]byte, base64.StdEncoding.EncodedLen(128)) | ||
base64.StdEncoding.Encode(encodedKey, buf) | ||
|
||
wm, err = kr.Update(&RootKey{ | ||
Key: string(encodedKey), | ||
Meta: &RootKeyMeta{ | ||
KeyID: id, | ||
Active: true, | ||
Algorithm: EncryptionAlgorithmAES256GCM, | ||
EncryptionsCount: 100, | ||
}}, nil) | ||
require.NoError(t, err) | ||
assertWriteMeta(t, wm) | ||
|
||
// Delete the old key | ||
wm, err = kr.Delete(&KeyringDeleteOptions{KeyID: keys[0].KeyID}, nil) | ||
require.NoError(t, err) | ||
assertWriteMeta(t, wm) | ||
|
||
// Read all the keys back | ||
keys, qm, err = kr.List(&QueryOptions{WaitIndex: key.CreateIndex}) | ||
require.NoError(t, err) | ||
assertQueryMeta(t, qm) | ||
// TODO: there'll be 2 keys here once we get bootstrapping done | ||
require.Len(t, keys, 1) | ||
require.Equal(t, id, keys[0].KeyID) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package agent | ||
|
||
import ( | ||
"encoding/base64" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/hashicorp/nomad/api" | ||
"github.com/hashicorp/nomad/nomad/structs" | ||
) | ||
|
||
// KeyringRequest is used route operator/raft API requests to the implementing | ||
// functions. | ||
func (s *HTTPServer) KeyringRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { | ||
|
||
path := strings.TrimPrefix(req.URL.Path, "/v1/operator/keyring/") | ||
switch { | ||
case strings.HasPrefix(path, "keys"): | ||
switch req.Method { | ||
case http.MethodGet: | ||
return s.keyringListRequest(resp, req) | ||
case http.MethodPost, http.MethodPut: | ||
return s.keyringUpsertRequest(resp, req) | ||
default: | ||
return nil, CodedError(405, ErrInvalidMethod) | ||
} | ||
case strings.HasPrefix(path, "key"): | ||
keyID := strings.TrimPrefix(req.URL.Path, "/v1/operator/keyring/key/") | ||
switch req.Method { | ||
case http.MethodDelete: | ||
return s.keyringDeleteRequest(resp, req, keyID) | ||
default: | ||
return nil, CodedError(405, ErrInvalidMethod) | ||
} | ||
case strings.HasPrefix(path, "rotate"): | ||
return s.keyringRotateRequest(resp, req) | ||
default: | ||
return nil, CodedError(405, ErrInvalidMethod) | ||
} | ||
} | ||
|
||
func (s *HTTPServer) keyringListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { | ||
|
||
args := structs.KeyringListRootKeyMetaRequest{} | ||
if s.parse(resp, req, &args.Region, &args.QueryOptions) { | ||
return nil, nil | ||
} | ||
|
||
var out structs.KeyringListRootKeyMetaResponse | ||
if err := s.agent.RPC("Keyring.List", &args, &out); err != nil { | ||
return nil, err | ||
} | ||
|
||
setMeta(resp, &out.QueryMeta) | ||
if out.Keys == nil { | ||
out.Keys = make([]*structs.RootKeyMeta, 0) | ||
} | ||
return out.Keys, nil | ||
} | ||
|
||
func (s *HTTPServer) keyringRotateRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { | ||
|
||
args := structs.KeyringRotateRootKeyRequest{} | ||
s.parseWriteRequest(req, &args.WriteRequest) | ||
|
||
query := req.URL.Query() | ||
switch query.Get("algo") { | ||
case string(structs.EncryptionAlgorithmAES256GCM): | ||
args.Algorithm = structs.EncryptionAlgorithmAES256GCM | ||
case string(structs.EncryptionAlgorithmXChaCha20): | ||
args.Algorithm = structs.EncryptionAlgorithmXChaCha20 | ||
} | ||
|
||
if _, ok := query["full"]; ok { | ||
args.Full = true | ||
} | ||
|
||
var out structs.KeyringRotateRootKeyResponse | ||
if err := s.agent.RPC("Keyring.Rotate", &args, &out); err != nil { | ||
return nil, err | ||
} | ||
setIndex(resp, out.Index) | ||
return out, nil | ||
} | ||
|
||
func (s *HTTPServer) keyringUpsertRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { | ||
|
||
var key api.RootKey | ||
if err := decodeBody(req, &key); err != nil { | ||
return nil, CodedError(400, err.Error()) | ||
} | ||
if key.Meta == nil { | ||
return nil, CodedError(400, "decoded key did not include metadata") | ||
} | ||
|
||
decodedKey := make([]byte, base64.StdEncoding.DecodedLen(len(key.Key))) | ||
_, err := base64.StdEncoding.Decode(decodedKey, []byte(key.Key)) | ||
if err != nil { | ||
return nil, CodedError(400, fmt.Sprintf("could not decode key: %v", err)) | ||
} | ||
|
||
args := structs.KeyringUpdateRootKeyRequest{ | ||
RootKey: &structs.RootKey{ | ||
Key: decodedKey, | ||
Meta: &structs.RootKeyMeta{ | ||
Active: key.Meta.Active, | ||
KeyID: key.Meta.KeyID, | ||
Algorithm: structs.EncryptionAlgorithm(key.Meta.Algorithm), | ||
EncryptionsCount: key.Meta.EncryptionsCount, | ||
}, | ||
}, | ||
} | ||
s.parseWriteRequest(req, &args.WriteRequest) | ||
|
||
var out structs.KeyringUpdateRootKeyResponse | ||
if err := s.agent.RPC("Keyring.Update", &args, &out); err != nil { | ||
return nil, err | ||
} | ||
setIndex(resp, out.Index) | ||
return out, nil | ||
} | ||
|
||
func (s *HTTPServer) keyringDeleteRequest(resp http.ResponseWriter, req *http.Request, keyID string) (interface{}, error) { | ||
|
||
args := structs.KeyringDeleteRootKeyRequest{KeyID: keyID} | ||
s.parseWriteRequest(req, &args.WriteRequest) | ||
|
||
var out structs.KeyringDeleteRootKeyResponse | ||
if err := s.agent.RPC("Keyring.Delete", &args, &out); err != nil { | ||
return nil, err | ||
} | ||
setIndex(resp, out.Index) | ||
return out, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package agent | ||
|
||
import ( | ||
"encoding/base64" | ||
"math/rand" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/hashicorp/nomad/api" | ||
"github.com/hashicorp/nomad/ci" | ||
"github.com/hashicorp/nomad/helper/uuid" | ||
"github.com/hashicorp/nomad/nomad/structs" | ||
) | ||
|
||
func TestHTTP_Keyring_CRUD(t *testing.T) { | ||
ci.Parallel(t) | ||
|
||
httpTest(t, nil, func(s *TestAgent) { | ||
|
||
respW := httptest.NewRecorder() | ||
|
||
// Rotate | ||
|
||
req, err := http.NewRequest(http.MethodPut, "/v1/operator/keyring/rotate", nil) | ||
require.NoError(t, err) | ||
obj, err := s.Server.KeyringRequest(respW, req) | ||
require.NoError(t, err) | ||
require.NotZero(t, respW.HeaderMap.Get("X-Nomad-Index")) | ||
rotateResp := obj.(structs.KeyringRotateRootKeyResponse) | ||
require.NotNil(t, rotateResp.Key) | ||
require.True(t, rotateResp.Key.Active) | ||
|
||
// List | ||
|
||
req, err = http.NewRequest(http.MethodGet, "/v1/operator/keyring/keys", nil) | ||
require.NoError(t, err) | ||
obj, err = s.Server.KeyringRequest(respW, req) | ||
require.NoError(t, err) | ||
listResp := obj.([]*structs.RootKeyMeta) | ||
require.Len(t, listResp, 1) | ||
require.True(t, listResp[0].Active) | ||
|
||
// Update | ||
|
||
keyMeta := rotateResp.Key | ||
keyBuf := make([]byte, 128) | ||
rand.Read(keyBuf) | ||
encodedKey := make([]byte, base64.StdEncoding.EncodedLen(128)) | ||
base64.StdEncoding.Encode(encodedKey, keyBuf) | ||
|
||
newID := uuid.Generate() | ||
|
||
key := &api.RootKey{ | ||
Meta: &api.RootKeyMeta{ | ||
Active: true, | ||
KeyID: newID, | ||
Algorithm: api.EncryptionAlgorithm(keyMeta.Algorithm), | ||
EncryptionsCount: 500, | ||
}, | ||
Key: string(encodedKey), | ||
} | ||
reqBuf := encodeReq(key) | ||
|
||
req, err = http.NewRequest(http.MethodPut, "/v1/operator/keyring/keys", reqBuf) | ||
require.NoError(t, err) | ||
obj, err = s.Server.KeyringRequest(respW, req) | ||
require.NoError(t, err) | ||
updateResp := obj.(structs.KeyringUpdateRootKeyResponse) | ||
require.NotNil(t, updateResp) | ||
|
||
// Delete the old key and verify its gone | ||
|
||
id := rotateResp.Key.KeyID | ||
req, err = http.NewRequest(http.MethodDelete, "/v1/operator/keyring/key/"+id, nil) | ||
require.NoError(t, err) | ||
obj, err = s.Server.KeyringRequest(respW, req) | ||
require.NoError(t, err) | ||
|
||
req, err = http.NewRequest(http.MethodGet, "/v1/operator/keyring/keys", nil) | ||
require.NoError(t, err) | ||
obj, err = s.Server.KeyringRequest(respW, req) | ||
require.NoError(t, err) | ||
listResp = obj.([]*structs.RootKeyMeta) | ||
require.Len(t, listResp, 1) | ||
require.True(t, listResp[0].Active) | ||
require.Equal(t, newID, listResp[0].KeyID) | ||
|
||
}) | ||
} |