mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
keyring HTTP API (#13077)
This commit is contained in:
94
api/keyring.go
Normal file
94
api/keyring.go
Normal file
@@ -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, 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
|
||||
}
|
||||
64
api/keyring_test.go
Normal file
64
api/keyring_test.go
Normal file
@@ -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)
|
||||
|
||||
}
|
||||
@@ -397,9 +397,9 @@ func (s HTTPServer) registerHandlers(enableDebug bool) {
|
||||
|
||||
s.mux.HandleFunc("/v1/search/fuzzy", s.wrap(s.FuzzySearchRequest))
|
||||
s.mux.HandleFunc("/v1/search", s.wrap(s.SearchRequest))
|
||||
|
||||
s.mux.HandleFunc("/v1/operator/license", s.wrap(s.LicenseRequest))
|
||||
s.mux.HandleFunc("/v1/operator/raft/", s.wrap(s.OperatorRequest))
|
||||
s.mux.HandleFunc("/v1/operator/keyring/", s.wrap(s.KeyringRequest))
|
||||
s.mux.HandleFunc("/v1/operator/autopilot/configuration", s.wrap(s.OperatorAutopilotConfiguration))
|
||||
s.mux.HandleFunc("/v1/operator/autopilot/health", s.wrap(s.OperatorServerHealth))
|
||||
s.mux.HandleFunc("/v1/operator/snapshot", s.wrap(s.SnapshotRequest))
|
||||
|
||||
135
command/agent/keyring_endpoint.go
Normal file
135
command/agent/keyring_endpoint.go
Normal file
@@ -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
|
||||
}
|
||||
92
command/agent/keyring_endpoint_test.go
Normal file
92
command/agent/keyring_endpoint_test.go
Normal file
@@ -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)
|
||||
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user