mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-02-04 17:15:26 -05:00
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:
parent
7cada2c9e8
commit
3177b2fdb7
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- GCP-native Kubernetes load balancing
|
- GCP-native Kubernetes load balancing
|
||||||
|
|
||||||
### Changed
|
### 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
|
### Removed
|
||||||
|
|
||||||
|
7
cli/internal/cmd/details.go
Normal file
7
cli/internal/cmd/details.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
type clusterIDFile struct {
|
||||||
|
ClusterID string
|
||||||
|
OwnerID string
|
||||||
|
Endpoint string
|
||||||
|
}
|
@ -267,6 +267,11 @@ func (r activationResult) writeOutput(wr io.Writer, fileHandler file.Handler) er
|
|||||||
return fmt.Errorf("write kubeconfig: %w", err)
|
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.Fprintln(wr, "You can now connect to your cluster by executing:")
|
||||||
fmt.Fprintf(wr, "\twg-quick up ./%s\n", constants.WGQuickConfigFilename)
|
fmt.Fprintf(wr, "\twg-quick up ./%s\n", constants.WGQuickConfigFilename)
|
||||||
fmt.Fprintf(wr, "\texport KUBECONFIG=\"$PWD/%s\"\n", constants.AdminConfFilename)
|
fmt.Fprintf(wr, "\texport KUBECONFIG=\"$PWD/%s\"\n", constants.AdminConfFilename)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -331,6 +332,13 @@ func TestWriteOutput(t *testing.T) {
|
|||||||
coordinatorPubIP: "baz-qq",
|
coordinatorPubIP: "baz-qq",
|
||||||
kubeconfig: "foo-bar-baz-qq",
|
kubeconfig: "foo-bar-baz-qq",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedIdFile := clusterIDFile{
|
||||||
|
Endpoint: result.coordinatorPubIP,
|
||||||
|
ClusterID: result.clusterID,
|
||||||
|
OwnerID: result.ownerID,
|
||||||
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
testFs := afero.NewMemMapFs()
|
testFs := afero.NewMemMapFs()
|
||||||
fileHandler := file.NewHandler(testFs)
|
fileHandler := file.NewHandler(testFs)
|
||||||
@ -340,11 +348,20 @@ func TestWriteOutput(t *testing.T) {
|
|||||||
assert.Contains(out.String(), result.clientVpnIP)
|
assert.Contains(out.String(), result.clientVpnIP)
|
||||||
assert.Contains(out.String(), result.coordinatorPubIP)
|
assert.Contains(out.String(), result.coordinatorPubIP)
|
||||||
assert.Contains(out.String(), result.coordinatorPubKey)
|
assert.Contains(out.String(), result.coordinatorPubKey)
|
||||||
|
assert.Contains(out.String(), result.clusterID)
|
||||||
|
assert.Contains(out.String(), result.ownerID)
|
||||||
|
|
||||||
afs := afero.Afero{Fs: testFs}
|
afs := afero.Afero{Fs: testFs}
|
||||||
adminConf, err := afs.ReadFile(constants.AdminConfFilename)
|
adminConf, err := afs.ReadFile(constants.AdminConfFilename)
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.Equal(result.kubeconfig, string(adminConf))
|
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) {
|
func TestIpsToEndpoints(t *testing.T) {
|
||||||
|
@ -58,6 +58,10 @@ func validInstanceTypeForProvider(cmd *cobra.Command, insType string, provider c
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateEndpoint(endpoint string, defaultPort int) (string, error) {
|
func validateEndpoint(endpoint string, defaultPort int) (string, error) {
|
||||||
|
if endpoint == "" {
|
||||||
|
return "", errors.New("endpoint is empty")
|
||||||
|
}
|
||||||
|
|
||||||
_, _, err := net.SplitHostPort(endpoint)
|
_, _, err := net.SplitHostPort(endpoint)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return endpoint, nil
|
return endpoint, nil
|
||||||
|
@ -65,6 +65,11 @@ func TestValidateEndpoint(t *testing.T) {
|
|||||||
defaultPort: 3,
|
defaultPort: 3,
|
||||||
wantResult: "foo:3",
|
wantResult: "foo:3",
|
||||||
},
|
},
|
||||||
|
"empty endpoint": {
|
||||||
|
endpoint: "",
|
||||||
|
defaultPort: 3,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
"invalid endpoint": {
|
"invalid endpoint": {
|
||||||
endpoint: "foo:2:2",
|
endpoint: "foo:2:2",
|
||||||
defaultPort: 3,
|
defaultPort: 3,
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/cli/internal/cloudcmd"
|
"github.com/edgelesssys/constellation/cli/internal/cloudcmd"
|
||||||
@ -25,7 +26,9 @@ func NewVerifyCmd() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "verify {aws|azure|gcp}",
|
Use: "verify {aws|azure|gcp}",
|
||||||
Short: "Verify the confidential properties of a Constellation cluster",
|
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(
|
Args: cobra.MatchAll(
|
||||||
cobra.ExactArgs(1),
|
cobra.ExactArgs(1),
|
||||||
isCloudProvider(0),
|
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("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().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)")
|
cmd.Flags().StringP("node-endpoint", "e", "", "endpoint of the node to verify, passed as HOST[:PORT]")
|
||||||
must(cmd.MarkFlagRequired("node-endpoint"))
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ func runVerify(cmd *cobra.Command, args []string) error {
|
|||||||
func verify(
|
func verify(
|
||||||
cmd *cobra.Command, provider cloudprovider.Provider, fileHandler file.Handler, verifyClient verifyClient,
|
cmd *cobra.Command, provider cloudprovider.Provider, fileHandler file.Handler, verifyClient verifyClient,
|
||||||
) error {
|
) error {
|
||||||
flags, err := parseVerifyFlags(cmd)
|
flags, err := parseVerifyFlags(cmd, fileHandler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -97,7 +99,11 @@ func verify(
|
|||||||
return nil
|
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")
|
ownerID, err := cmd.Flags().GetString("owner-id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return verifyFlags{}, fmt.Errorf("parsing owner-id argument: %w", err)
|
return verifyFlags{}, fmt.Errorf("parsing owner-id argument: %w", err)
|
||||||
@ -106,22 +112,37 @@ func parseVerifyFlags(cmd *cobra.Command) (verifyFlags, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return verifyFlags{}, fmt.Errorf("parsing unique-id argument: %w", err)
|
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")
|
endpoint, err := cmd.Flags().GetString("node-endpoint")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return verifyFlags{}, fmt.Errorf("parsing node-endpoint argument: %w", err)
|
return verifyFlags{}, fmt.Errorf("parsing node-endpoint argument: %w", err)
|
||||||
}
|
}
|
||||||
endpoint, err = validateEndpoint(endpoint, constants.VerifyServiceNodePortGRPC)
|
|
||||||
if err != nil {
|
// Get empty values from ID file
|
||||||
return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err)
|
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 {
|
if err != nil {
|
||||||
return verifyFlags{}, fmt.Errorf("parsing config path argument: %w", err)
|
return verifyFlags{}, fmt.Errorf("validating endpoint argument: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyFlags{
|
return verifyFlags{
|
||||||
@ -139,6 +160,14 @@ type verifyFlags struct {
|
|||||||
configPath string
|
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
|
// verifyCompletion handles the completion of CLI arguments. It is frequently called
|
||||||
// while the user types arguments of the command to suggest completion.
|
// while the user types arguments of the command to suggest completion.
|
||||||
func verifyCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
func verifyCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
@ -61,11 +61,13 @@ func TestVerify(t *testing.T) {
|
|||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
setupFs func(*require.Assertions) afero.Fs
|
setupFs func(*require.Assertions) afero.Fs
|
||||||
provider cloudprovider.Provider
|
provider cloudprovider.Provider
|
||||||
protoClient verifyClient
|
protoClient *stubVerifyClient
|
||||||
nodeEndpointFlag string
|
nodeEndpointFlag string
|
||||||
configFlag string
|
configFlag string
|
||||||
ownerIDFlag string
|
ownerIDFlag string
|
||||||
clusterIDFlag string
|
clusterIDFlag string
|
||||||
|
idFile *clusterIDFile
|
||||||
|
wantEndpoint string
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
"gcp": {
|
"gcp": {
|
||||||
@ -74,6 +76,7 @@ func TestVerify(t *testing.T) {
|
|||||||
nodeEndpointFlag: "192.0.2.1:1234",
|
nodeEndpointFlag: "192.0.2.1:1234",
|
||||||
ownerIDFlag: zeroBase64,
|
ownerIDFlag: zeroBase64,
|
||||||
protoClient: &stubVerifyClient{},
|
protoClient: &stubVerifyClient{},
|
||||||
|
wantEndpoint: "192.0.2.1:1234",
|
||||||
},
|
},
|
||||||
"azure": {
|
"azure": {
|
||||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
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",
|
nodeEndpointFlag: "192.0.2.1:1234",
|
||||||
ownerIDFlag: zeroBase64,
|
ownerIDFlag: zeroBase64,
|
||||||
protoClient: &stubVerifyClient{},
|
protoClient: &stubVerifyClient{},
|
||||||
|
wantEndpoint: "192.0.2.1:1234",
|
||||||
},
|
},
|
||||||
"default port": {
|
"default port": {
|
||||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||||
@ -88,6 +92,31 @@ func TestVerify(t *testing.T) {
|
|||||||
nodeEndpointFlag: "192.0.2.1",
|
nodeEndpointFlag: "192.0.2.1",
|
||||||
ownerIDFlag: zeroBase64,
|
ownerIDFlag: zeroBase64,
|
||||||
protoClient: &stubVerifyClient{},
|
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": {
|
"invalid endpoint": {
|
||||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
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",
|
nodeEndpointFlag: "192.0.2.1:1234",
|
||||||
wantErr: true,
|
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": {
|
"config file not existing": {
|
||||||
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
|
||||||
provider: cloudprovider.GCP,
|
provider: cloudprovider.GCP,
|
||||||
@ -153,6 +190,10 @@ func TestVerify(t *testing.T) {
|
|||||||
}
|
}
|
||||||
fileHandler := file.NewHandler(tc.setupFs(require))
|
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)
|
err := verify(cmd, tc.provider, fileHandler, tc.protoClient)
|
||||||
|
|
||||||
if tc.wantErr {
|
if tc.wantErr {
|
||||||
@ -160,6 +201,7 @@ func TestVerify(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.Contains(out.String(), "OK")
|
assert.Contains(out.String(), "OK")
|
||||||
|
assert.Equal(tc.wantEndpoint, tc.protoClient.endpoint)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -284,9 +326,11 @@ func TestVerifyClient(t *testing.T) {
|
|||||||
|
|
||||||
type stubVerifyClient struct {
|
type stubVerifyClient struct {
|
||||||
verifyErr error
|
verifyErr error
|
||||||
|
endpoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *stubVerifyClient) Verify(ctx context.Context, endpoint string, req *verifyproto.GetAttestationRequest, validator atls.Validator) error {
|
func (c *stubVerifyClient) Verify(ctx context.Context, endpoint string, req *verifyproto.GetAttestationRequest, validator atls.Validator) error {
|
||||||
|
c.endpoint = endpoint
|
||||||
return c.verifyErr
|
return c.verifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ const (
|
|||||||
//
|
//
|
||||||
|
|
||||||
StateFilename = "constellation-state.json"
|
StateFilename = "constellation-state.json"
|
||||||
|
IDsFileName = "constellation-id.json"
|
||||||
ConfigFilename = "constellation-conf.yaml"
|
ConfigFilename = "constellation-conf.yaml"
|
||||||
DebugdConfigFilename = "cdbg-conf.yaml"
|
DebugdConfigFilename = "cdbg-conf.yaml"
|
||||||
AdminConfFilename = "constellation-admin.conf"
|
AdminConfFilename = "constellation-admin.conf"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user