AB#2032 Write IDs to disk and read when verifying (#212)

* AB#2032 Write IDs to disk and read when verifying

* Update CHANGELOG.md

* update changelog

* update changelog

* cli verify: prefer flag values

* Rename fid file

Co-authored-by: Thomas Tendyck <tt@edgeless.systems>
This commit is contained in:
cm 2022-07-01 10:57:29 +02:00 committed by GitHub
parent 7cada2c9e8
commit 3177b2fdb7
9 changed files with 128 additions and 15 deletions

View File

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- GCP-native Kubernetes load balancing
### Changed
- Create `constellation-id.json` when initializing the cluster to save the cluster's unique ID and the owner ID to disk. Verifying will read this file back to use the values for the verification. This is overriden by specifying the command line arguments.
### Removed

View File

@ -0,0 +1,7 @@
package cmd
type clusterIDFile struct {
ClusterID string
OwnerID string
Endpoint string
}

View File

@ -267,6 +267,11 @@ func (r activationResult) writeOutput(wr io.Writer, fileHandler file.Handler) er
return fmt.Errorf("write kubeconfig: %w", err)
}
idFile := clusterIDFile{ClusterID: r.clusterID, OwnerID: r.ownerID, Endpoint: r.coordinatorPubIP}
if err := fileHandler.WriteJSON(constants.IDsFileName, idFile, file.OptNone); err != nil {
return fmt.Errorf("writing Constellation id file: %w", err)
}
fmt.Fprintln(wr, "You can now connect to your cluster by executing:")
fmt.Fprintf(wr, "\twg-quick up ./%s\n", constants.WGQuickConfigFilename)
fmt.Fprintf(wr, "\texport KUBECONFIG=\"$PWD/%s\"\n", constants.AdminConfFilename)

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"strconv"
"strings"
@ -331,6 +332,13 @@ func TestWriteOutput(t *testing.T) {
coordinatorPubIP: "baz-qq",
kubeconfig: "foo-bar-baz-qq",
}
expectedIdFile := clusterIDFile{
Endpoint: result.coordinatorPubIP,
ClusterID: result.clusterID,
OwnerID: result.ownerID,
}
var out bytes.Buffer
testFs := afero.NewMemMapFs()
fileHandler := file.NewHandler(testFs)
@ -340,11 +348,20 @@ func TestWriteOutput(t *testing.T) {
assert.Contains(out.String(), result.clientVpnIP)
assert.Contains(out.String(), result.coordinatorPubIP)
assert.Contains(out.String(), result.coordinatorPubKey)
assert.Contains(out.String(), result.clusterID)
assert.Contains(out.String(), result.ownerID)
afs := afero.Afero{Fs: testFs}
adminConf, err := afs.ReadFile(constants.AdminConfFilename)
assert.NoError(err)
assert.Equal(result.kubeconfig, string(adminConf))
idsFile, err := afs.ReadFile(constants.IDsFileName)
assert.NoError(err)
var testIdFile clusterIDFile
err = json.Unmarshal(idsFile, &testIdFile)
assert.NoError(err)
assert.Equal(expectedIdFile, testIdFile)
}
func TestIpsToEndpoints(t *testing.T) {

View File

@ -58,6 +58,10 @@ func validInstanceTypeForProvider(cmd *cobra.Command, insType string, provider c
}
func validateEndpoint(endpoint string, defaultPort int) (string, error) {
if endpoint == "" {
return "", errors.New("endpoint is empty")
}
_, _, err := net.SplitHostPort(endpoint)
if err == nil {
return endpoint, nil

View File

@ -65,6 +65,11 @@ func TestValidateEndpoint(t *testing.T) {
defaultPort: 3,
wantResult: "foo:3",
},
"empty endpoint": {
endpoint: "",
defaultPort: 3,
wantErr: true,
},
"invalid endpoint": {
endpoint: "foo:2:2",
defaultPort: 3,

View File

@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"io/fs"
"net"
"github.com/edgelesssys/constellation/cli/internal/cloudcmd"
@ -25,7 +26,9 @@ func NewVerifyCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "verify {aws|azure|gcp}",
Short: "Verify the confidential properties of a Constellation cluster",
Long: "Verify the confidential properties of a Constellation cluster.",
Long: `Verify the confidential properties of a Constellation cluster.
If arguments are not specified, values are read from ` + constants.IDsFileName + `.`,
Args: cobra.MatchAll(
cobra.ExactArgs(1),
isCloudProvider(0),
@ -35,8 +38,7 @@ func NewVerifyCmd() *cobra.Command {
}
cmd.Flags().String("owner-id", "", "verify using the owner identity derived from the master secret")
cmd.Flags().String("unique-id", "", "verify using the unique cluster identity")
cmd.Flags().StringP("node-endpoint", "e", "", "endpoint of the node to verify, passed as HOST[:PORT] (required)")
must(cmd.MarkFlagRequired("node-endpoint"))
cmd.Flags().StringP("node-endpoint", "e", "", "endpoint of the node to verify, passed as HOST[:PORT]")
return cmd
}
@ -50,7 +52,7 @@ func runVerify(cmd *cobra.Command, args []string) error {
func verify(
cmd *cobra.Command, provider cloudprovider.Provider, fileHandler file.Handler, verifyClient verifyClient,
) error {
flags, err := parseVerifyFlags(cmd)
flags, err := parseVerifyFlags(cmd, fileHandler)
if err != nil {
return err
}
@ -97,7 +99,11 @@ func verify(
return nil
}
func parseVerifyFlags(cmd *cobra.Command) (verifyFlags, error) {
func parseVerifyFlags(cmd *cobra.Command, fileHandler file.Handler) (verifyFlags, error) {
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing config path argument: %w", err)
}
ownerID, err := cmd.Flags().GetString("owner-id")
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing owner-id argument: %w", err)
@ -106,22 +112,37 @@ func parseVerifyFlags(cmd *cobra.Command) (verifyFlags, error) {
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing unique-id argument: %w", err)
}
if ownerID == "" && clusterID == "" {
return verifyFlags{}, errors.New("neither owner-id nor unique-id provided to verify the cluster")
}
endpoint, err := cmd.Flags().GetString("node-endpoint")
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing node-endpoint argument: %w", err)
}
endpoint, err = validateEndpoint(endpoint, constants.VerifyServiceNodePortGRPC)
if err != nil {
return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err)
// Get empty values from ID file
emptyEndpoint := endpoint == ""
emptyIDs := ownerID == "" && clusterID == ""
if emptyEndpoint || emptyIDs {
if details, err := readIds(fileHandler); err == nil {
if emptyEndpoint {
cmd.Printf("Using endpoint from %q. Specify --node-endpoint to override this.\n", constants.IDsFileName)
endpoint = details.Endpoint
}
if emptyIDs {
cmd.Printf("Using IDs from %q. Specify --owner-id and/or --unique-id to override this.\n", constants.IDsFileName)
ownerID = details.OwnerID
clusterID = details.ClusterID
}
} else if !errors.Is(err, fs.ErrNotExist) {
return verifyFlags{}, err
}
}
configPath, err := cmd.Flags().GetString("config")
// Validate
if ownerID == "" && clusterID == "" {
return verifyFlags{}, errors.New("neither owner-id nor unique-id provided to verify the cluster")
}
endpoint, err = validateEndpoint(endpoint, constants.CoordinatorPort)
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing config path argument: %w", err)
return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err)
}
return verifyFlags{
@ -139,6 +160,14 @@ type verifyFlags struct {
configPath string
}
func readIds(fileHandler file.Handler) (clusterIDFile, error) {
det := clusterIDFile{}
if err := fileHandler.ReadJSON(constants.IDsFileName, &det); err != nil {
return clusterIDFile{}, fmt.Errorf("reading cluster ids: %w", err)
}
return det, nil
}
// verifyCompletion handles the completion of CLI arguments. It is frequently called
// while the user types arguments of the command to suggest completion.
func verifyCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {

View File

@ -61,11 +61,13 @@ func TestVerify(t *testing.T) {
testCases := map[string]struct {
setupFs func(*require.Assertions) afero.Fs
provider cloudprovider.Provider
protoClient verifyClient
protoClient *stubVerifyClient
nodeEndpointFlag string
configFlag string
ownerIDFlag string
clusterIDFlag string
idFile *clusterIDFile
wantEndpoint string
wantErr bool
}{
"gcp": {
@ -74,6 +76,7 @@ func TestVerify(t *testing.T) {
nodeEndpointFlag: "192.0.2.1:1234",
ownerIDFlag: zeroBase64,
protoClient: &stubVerifyClient{},
wantEndpoint: "192.0.2.1:1234",
},
"azure": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
@ -81,6 +84,7 @@ func TestVerify(t *testing.T) {
nodeEndpointFlag: "192.0.2.1:1234",
ownerIDFlag: zeroBase64,
protoClient: &stubVerifyClient{},
wantEndpoint: "192.0.2.1:1234",
},
"default port": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
@ -88,6 +92,31 @@ func TestVerify(t *testing.T) {
nodeEndpointFlag: "192.0.2.1",
ownerIDFlag: zeroBase64,
protoClient: &stubVerifyClient{},
wantEndpoint: "192.0.2.1:9000",
},
"endpoint not set": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.GCP,
ownerIDFlag: zeroBase64,
protoClient: &stubVerifyClient{},
wantErr: true,
},
"endpoint from id file": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.GCP,
ownerIDFlag: zeroBase64,
protoClient: &stubVerifyClient{},
idFile: &clusterIDFile{Endpoint: "192.0.2.1:1234"},
wantEndpoint: "192.0.2.1:1234",
},
"override endpoint from details file": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.GCP,
nodeEndpointFlag: "192.0.2.2:1234",
ownerIDFlag: zeroBase64,
protoClient: &stubVerifyClient{},
idFile: &clusterIDFile{Endpoint: "192.0.2.1:1234"},
wantEndpoint: "192.0.2.2:1234",
},
"invalid endpoint": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
@ -103,6 +132,14 @@ func TestVerify(t *testing.T) {
nodeEndpointFlag: "192.0.2.1:1234",
wantErr: true,
},
"use owner id from id file": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.GCP,
nodeEndpointFlag: "192.0.2.1:1234",
protoClient: &stubVerifyClient{},
idFile: &clusterIDFile{OwnerID: zeroBase64},
wantEndpoint: "192.0.2.1:1234",
},
"config file not existing": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
provider: cloudprovider.GCP,
@ -153,6 +190,10 @@ func TestVerify(t *testing.T) {
}
fileHandler := file.NewHandler(tc.setupFs(require))
if tc.idFile != nil {
require.NoError(fileHandler.WriteJSON(constants.IDsFileName, tc.idFile, file.OptNone))
}
err := verify(cmd, tc.provider, fileHandler, tc.protoClient)
if tc.wantErr {
@ -160,6 +201,7 @@ func TestVerify(t *testing.T) {
} else {
assert.NoError(err)
assert.Contains(out.String(), "OK")
assert.Equal(tc.wantEndpoint, tc.protoClient.endpoint)
}
})
}
@ -284,9 +326,11 @@ func TestVerifyClient(t *testing.T) {
type stubVerifyClient struct {
verifyErr error
endpoint string
}
func (c *stubVerifyClient) Verify(ctx context.Context, endpoint string, req *verifyproto.GetAttestationRequest, validator atls.Validator) error {
c.endpoint = endpoint
return c.verifyErr
}

View File

@ -50,6 +50,7 @@ const (
//
StateFilename = "constellation-state.json"
IDsFileName = "constellation-id.json"
ConfigFilename = "constellation-conf.yaml"
DebugdConfigFilename = "cdbg-conf.yaml"
AdminConfFilename = "constellation-admin.conf"