Implement secrets for the secrets engine
In the Define roles for the secrets
engine tutorial, you added the
role/*
path to your secrets engine.
In this tutorial, you will create the workflows to allow Vault to renew and revoke the HashiCups API token. Then, you can add the HashiCups JSON Web Token (JWT) to the secrets engine's backend. A secret defines the type of secret a secrets engine should store and manage, such as a token, password, username, SSH key, and more.
To do this, you will:
- Set up your development environment.
You will clone the HashiCups secrets engine repository. This contains many of the interfaces and objects you need to create a secrets engine. - Explore the attributes for the secret.
You will examine the attributes defined in the secrets engine for the HashiCups API token. - Define a secret for the backend.
You will define a secret and its fields to store into the secrets engine backend. - Implement a workflow to revoke the secret.
You will create a workflow to invalidate the HashiCups API token and remove it from the secrets engine backend. - Implement a workflow to renew the secret.
You will create a workflow to create the HashiCups API token and renew it in the secrets engine backend. - Add the secret type to the backend.
You will add a new secret for the HashiCups API token to the backend.
Prerequisites
- Golang 1.16+ installed and configured.
- Vault 1.8+ CLI installed locally.
Note
Complete the tutorial to define the roles for the secrets engine.
Set up your development environment
Clone the learn-vault-plugin-secrets-hashicups repository.
$ git clone https://github.com/hashicorp-education/learn-vault-plugin-secrets-hashicups
Change into the repository directory.
$ cd vault-plugin-secrets-hashicups
Note
If you are stuck in this tutorial, refer to the
vault-plugin-secrets-hashicups/solution
directory.
Explore the attributes for the secret
Open hashicups_token.go
. The file contains all of the objects
and methods related to creating and deleting the HashiCups API
token.
Examine the constant, which sets the name of the token type for
the Vault API as hashicups_token
.
hashicups_token.go
const ( hashiCupsTokenType = "hashicups_token") type hashiCupsToken struct { UserID int `json:"user_id"` Username string `json:"username"` TokenID string `json:"token_id"` Token string `json:"token"`}
Note
You can name your token type with any string value, but you cannot change it after you set it.
hashiCupsToken
defines the API token issued by the HashiCups API when you
sign in and out. It sets a few attributes, including:
UserID
: Generated when a user first signs up for HashiCups.Username
: Set by the user when they first sign up for HashiCups.TokenID
: A unique identifier for the token. The HashiCups API does not generate a token ID, so you must implement ID generation in the secrets engine properly test token revocation and renewal.Token
: JWT for the HashiCups API.
hashicups_token.go
const ( hashiCupsTokenType = "hashicups_token") type hashiCupsToken struct { UserID int `json:"user_id"` Username string `json:"username"` TokenID string `json:"token_id"` Token string `json:"token"`}
Note
You can define other attributes for your secret but consider including a unique identifier for your secrets type. The identifier helps you test and track revocations.
Define a secret for the backend
Open hashicups_token.go
.
Note
Replace the methods and structs in the scaffold with the embedded code examples.
Replace the method hashiCupsToken
, which extends the secrets engine backend,
with the token type and a schema for the Field
. You use a string constant to
define the Type
as hashicups_token
. You also set a string type for the
token
attribute in Fields
to store the JWT from HashiCups.
hashicups_token.go
func (b *hashiCupsBackend) hashiCupsToken() *framework.Secret { return &framework.Secret{ Type: hashiCupsTokenType, Fields: map[string]*framework.FieldSchema{ "token": { Type: framework.TypeString, Description: "HashiCups Token", }, }, }}
Note
Your own secrets engine can include other fields for a given secret, including password or keys.
Implement a workflow to revoke the secret
As you develop the secrets object for your secrets engine, you need to examine your target API to identify how to get a new secret (renew) and invalidate an existing one (revoke).
You need to use two endpoints from the HashiCups products API to renew and revoke JSON Web Tokens (JWTs) from HashiCups.
PATH | METHOD | DESCRIPTION | HEADER | REQUEST | RESPONSE |
---|---|---|---|---|---|
/signin | POST | Sign in an existing user and get a new JWT | {"username": "user", "password": "pass"} | {"UserID":1,"Username":"user","token":"<JWT>"} | |
/signout | POST | Sign out a user and invalidate a JWT | Authorization:<JWT> | Signed out user |
The HashiCups products client SDK implements the two interfaces for signing in and out of the HashiCups API.
Open hashicups_token.go
.
Note
Replace the methods and structs in the scaffold with the embedded code examples.
Add the deleteToken
method to use the HashiCups Go SDK client to sign out
from the HashiCups API. Signing out from the HashiCups API invalidates the
JWT and prevents someone from using it.
hashicups_token.go
func deleteToken(ctx context.Context, c *hashiCupsClient, token string) error { c.Client.Token = token err := c.SignOut() if err != nil { return nil } return nil}
Replace the tokenRevoke
method with new logic to delete the HashiCups API
token. The HashiCups API invalidates the token that you use when you sign out.
As a result, you pass the value of the token stored at req.Secret.InternalData
to `deleteToken.
hashicups_token.go
func (b *hashiCupsBackend) tokenRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { client, err := b.getClient(ctx, req.Storage) if err != nil { return nil, fmt.Errorf("error getting client: %w", err) } token := "" tokenRaw, ok := req.Secret.InternalData["token"] if ok { token, ok = tokenRaw.(string) if !ok { return nil, fmt.Errorf("invalid value for token in secret internal data") } } if err := deleteToken(ctx, client, token); err != nil { return nil, fmt.Errorf("error revoking user token: %w", err) } return nil, nil}
Note
The HashiCups API does not offer an ideal API for token revocation!
Ideally, you should be able to pass an identifier to the target API to delete the
secret. Your secrets engine must access the identifier at req.Secret.InternalData
and pass it to your secrets deletion method.
Update the hashiCupsToken
method to include a field for Revoke
. It should call
back to the function for tokenRevoke
.
hashicups_token.go
func (b *hashiCupsBackend) hashiCupsToken() *framework.Secret { return &framework.Secret{ Type: hashiCupsTokenType, Fields: map[string]*framework.FieldSchema{ "token": { Type: framework.TypeString, Description: "HashiCups Token", }, }, Revoke: b.tokenRevoke, }}
Implement a workflow to renew the secret
Open hashicups_token.go
.
Note
Replace the methods and structs in the scaffold with the embedded code examples.
Add the createToken
method to get a new API token from the HashiCups API.
You get a new JWT each time you sign into the API. However, it does
not generate a token identifier. Instead, the method creates a new token ID for
testing and manual tracking.
Return the hashiCupsToken
object with the UserID,
Username
, TokenID
, and Token
from the API response.
hashicups_token.go
func createToken(ctx context.Context, c *hashiCupsClient, username string) (*hashiCupsToken, error) { response, err := c.SignIn() if err != nil { return nil, fmt.Errorf("error creating HashiCups token: %w", err) } tokenID := uuid.New().String() return &hashiCupsToken{ UserID: response.UserID, Username: username, TokenID: tokenID, Token: response.Token, }, nil}
Replace the tokenRenew
method to verify that a role exists in the secrets
engine backend before HashiCups creates a token. You also pass the secrets
object as a response and reset the time to live (TTL) and maximum TTL for the
role.
hashicups_token.go
func (b *hashiCupsBackend) tokenRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { roleRaw, ok := req.Secret.InternalData["role"] if !ok { return nil, fmt.Errorf("secret is missing role internal data") } role := roleRaw.(string) roleEntry, err := b.getRole(ctx, req.Storage, role) if err != nil { return nil, fmt.Errorf("error retrieving role: %w", err) } if roleEntry == nil { return nil, errors.New("error retrieving role: role is nil") } resp := &logical.Response{Secret: req.Secret} if roleEntry.TTL > 0 { resp.Secret.TTL = roleEntry.TTL } if roleEntry.MaxTTL > 0 { resp.Secret.MaxTTL = roleEntry.MaxTTL } return resp, nil}
Update the hashiCupsToken
method to include a field for Renew
. It should call
back to the function for tokenRenew
.
hashicups_token.go
func (b *hashiCupsBackend) hashiCupsToken() *framework.Secret { return &framework.Secret{ Type: hashiCupsTokenType, Fields: map[string]*framework.FieldSchema{ "token": { Type: framework.TypeString, Description: "HashiCups Token", }, }, Revoke: b.tokenRevoke, Renew: b.tokenRenew, }}
Add the secret type to the backend
You must add the hashiCupsToken
as a Secret
to the backend.
Note
Replace the methods and structs in the scaffold with the embedded code examples.
Open backend.go
and replace backend
to add hashiCupsToken
to the list
of Secrets
.
backend.go
func backend() *hashiCupsBackend { var b = hashiCupsBackend{} b.Backend = &framework.Backend{ Help: strings.TrimSpace(backendHelp), PathsSpecial: &logical.Paths{ LocalStorage: []string{}, SealWrapStorage: []string{ "config", "role/*", }, }, Paths: framework.PathAppend( pathRole(&b), []*framework.Path{ pathConfig(&b), }, ), Secrets: []*framework.Secret{ b.hashiCupsToken(), }, BackendType: logical.TypeLogical, Invalidate: b.invalidate, } return &b}
Note
If you do not add the secret to the backend object, you may get a null pointer error when you run your secrets engine.
Next steps
Congratulations! You defined a new secret for your secrets engine.
If you are stuck in this tutorial, refer to the
plugins/vault-plugin-secrets-hashicups/solution
directory.
- To learn more about Vault plugins, refer to the Vault Plugin System Documentation.
- Define the secrets engine's credentials endpoint in the next tutorial.