keyring HTTP API (#13077)

This commit is contained in:
Tim Gross
2022-05-20 12:16:21 -04:00
parent 1348a76e4b
commit 0b0aa3efe8
5 changed files with 386 additions and 1 deletions

View File

@@ -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))

View 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
}

View 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)
})
}