From 4a6b67791b251a4b3f6f56059c3afe43a69a49ff Mon Sep 17 00:00:00 2001 From: David Bond Date: Tue, 7 Mar 2023 15:56:28 +0000 Subject: [PATCH] Add support for ephemeral keys (#51) This commit adds a new option when reading tailnet keys that allows for the creation of ephemeral keys. It also standardises the error handling across the backend and adds additional documentation for how to customise key options when reading. Closes #50 Signed-off-by: David Bond --- README.md | 30 +++++++++++++++++++++++++++++- backend/backend.go | 21 ++++++++++++++++----- backend/backend_test.go | 16 ++++++++++------ 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2a696db..2815117 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ $ vault write tailscale/config tailnet=$TAILNET api_key=$API_KEY Success! Data written to: tailscale/config ``` -2. Generate keys using the Vault CLI. +3. Generate keys using the Vault CLI. ```shell $ vault read tailscale/key @@ -54,3 +54,31 @@ key secret-key-data reusable false tags ``` + +### Key Options + +The following key/value pairs can be added to the end of the `vault read` command to configure key properties: + +#### Tags + +Tags to apply to the device that uses the authentication key + +``` +vault read tailscale/key tags=something:somewhere +``` + +#### Preauthorized + +If true, machines added to the tailnet with this key will not required authorization + +``` +vault read tailscale/key preauthorized=true +``` + +#### Ephemeral + +If true, nodes created with this key will be removed after a period of inactivity or when they disconnect from the Tailnet + +``` +vault read tailscale/key ephemeral=true +``` diff --git a/backend/backend.go b/backend/backend.go index bc129a2..f55e0cd 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -4,6 +4,7 @@ package backend import ( "context" + "errors" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" @@ -35,6 +36,7 @@ const ( tagsDescription = "Tags to apply to the device that uses the authentication key" preauthorizedDescription = "If true, machines added to the tailnet with this key will not required authorization" apiUrlDescription = "The URL of the Tailscale API" + ephemeralDescription = "If true, nodes created with this key will be removed after a period of inactivity or when they disconnect from the Tailnet" ) // Create a new logical.Backend implementation that can generate authentication keys for Tailscale devices. @@ -55,6 +57,10 @@ func Create(ctx context.Context, config *logical.BackendConfig) (logical.Backend Type: framework.TypeBool, Description: preauthorizedDescription, }, + "ephemeral": { + Type: framework.TypeBool, + Description: ephemeralDescription, + }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ @@ -122,6 +128,7 @@ func (b *Backend) GenerateKey(ctx context.Context, request *logical.Request, dat var capabilities tailscale.KeyCapabilities capabilities.Devices.Create.Tags = data.Get("tags").([]string) capabilities.Devices.Create.Preauthorized = data.Get("preauthorized").(bool) + capabilities.Devices.Create.Ephemeral = data.Get("ephemeral").(bool) key, err := client.CreateKey(ctx, capabilities) if err != nil { @@ -148,7 +155,7 @@ func (b *Backend) ReadConfiguration(ctx context.Context, request *logical.Reques case err != nil: return nil, err case entry == nil: - return logical.ErrorResponse("configuration has not been set"), nil + return nil, errors.New("configuration has not been set") } var config Config @@ -175,11 +182,11 @@ func (b *Backend) UpdateConfiguration(ctx context.Context, request *logical.Requ switch { case config.Tailnet == "": - return logical.ErrorResponse("provided tailnet cannot be empty"), nil + return nil, errors.New("provided tailnet cannot be empty") case config.APIKey == "": - return logical.ErrorResponse("provided api_key cannot be empty"), nil + return nil, errors.New("provided api_key cannot be empty") case config.APIUrl == "": - return logical.ErrorResponse("provided api_url cannot be empty"), nil + return nil, errors.New("provided api_url cannot be empty") } entry, err := logical.StorageEntryJSON(configPath, config) @@ -187,5 +194,9 @@ func (b *Backend) UpdateConfiguration(ctx context.Context, request *logical.Requ return nil, err } - return nil, request.Storage.Put(ctx, entry) + if err = request.Storage.Put(ctx, entry); err != nil { + return nil, err + } + + return &logical.Response{}, nil } diff --git a/backend/backend_test.go b/backend/backend_test.go index a27f4f9..22784fc 100644 --- a/backend/backend_test.go +++ b/backend/backend_test.go @@ -26,6 +26,9 @@ func TestBackend_GenerateKey(t *testing.T) { "preauthorized": { Type: framework.TypeBool, }, + "ephemeral": { + Type: framework.TypeBool, + }, } tt := []struct { @@ -75,10 +78,11 @@ func TestBackend_GenerateKey(t *testing.T) { response, err := b.GenerateKey(ctx, tc.Request, tc.Data) if tc.ExpectsError { - assert.Error(t, response.Error()) + assert.Error(t, err) return } + assert.NoError(t, err) assert.EqualValues(t, tc.Expected, response.Data) }) } @@ -125,13 +129,13 @@ func TestBackend_ReadConfiguration(t *testing.T) { } response, err := b.ReadConfiguration(ctx, tc.Request, tc.Data) - assert.NoError(t, err) if tc.ExpectsError { - assert.Error(t, response.Error()) + assert.Error(t, err) return } + assert.NoError(t, err) assert.EqualValues(t, tc.Expected, response.Data) }) } @@ -202,14 +206,14 @@ func TestBackend_UpdateConfiguration(t *testing.T) { for _, tc := range tt { t.Run(tc.Name, func(t *testing.T) { - response, err := b.UpdateConfiguration(ctx, tc.Request, tc.Data) - assert.NoError(t, err) + _, err := b.UpdateConfiguration(ctx, tc.Request, tc.Data) if tc.ExpectsError { - assert.Error(t, response.Error()) + assert.Error(t, err) return } + assert.NoError(t, err) assert.EqualValues(t, tc.Expected, getConfig(t, ctx, tc.Request)) }) }