| name | working-with-secrets |
| description | Guide to the secrets abstraction including the Store, SecretService registry, and how to add new named secret types |
| argument-hint | |
Working with Secrets
The secrets system uses a two-layer architecture: a Store that persists secrets securely, and a SecretService registry that describes how each named type maps to HTTP requests inside workspaces.
Overview
- Secret values live exclusively in the system keychain — never on disk
- Non-sensitive metadata (type, hosts, path, header descriptors, envs) is persisted to
<storage-dir>/secrets.json
- Named types (e.g.
github) derive all their descriptor fields from a registered SecretService
- The built-in
other type lets the user supply descriptor fields explicitly; only --host and --header are required
Key Components
- Store interface (
pkg/secret/secret.go): Create(CreateParams) error, List() ([]ListItem, error), Get(name string) (ListItem, string, error), Remove(name string) error
- Store implementation (
pkg/secret/store.go): writes the value to the keychain, metadata to secrets.json
- SecretService interface (
pkg/secretservice/secretservice.go): describes a named type — description, host pattern, path, header name, header template, env vars
- Registry (
pkg/secretservice/registry.go): maps names to SecretService implementations
- Centralized registration (
pkg/secretservicesetup/register.go): loads definitions from the embedded secretservices.json and exposes ListAvailable() and RegisterAll()
Storage Layout
<storage-dir>/
secrets.json # metadata only — no secret values on disk
Keychain entry: service=kdn, user=<secret-name>, password=<secret-value>
The keychain backend is platform-specific: GNOME Keyring on Linux, Keychain on macOS, DPAPI on Windows (via github.com/zalando/go-keyring).
Secret Types
| Type | How descriptor fields are resolved |
|---|
Named (e.g. github) | Taken from the registered SecretService automatically |
other | Required: --host, --header; optional: --path, --headerTemplate, --env |
Using the Store
import "github.com/openkaiden/kdn/pkg/secret"
store := secret.NewStore(absStorageDir)
err := store.Create(secret.CreateParams{
Name: "my-token",
Type: "github",
Value: "ghp_xxxx",
Description: "Personal access token",
})
For other type, supply the required descriptor fields; Path, HeaderTemplate, and Envs are optional:
err := store.Create(secret.CreateParams{
Name: "my-api-key",
Type: secret.TypeOther,
Value: "secret123",
Hosts: []string{"api.example.com"},
Header: "Authorization",
Envs: []string{"MY_API_KEY"},
})
Create checks for a duplicate name before touching the keychain. ErrSecretAlreadyExists is returned if the name is already taken.
To retrieve all stored secrets (metadata only — no secret values):
items, err := store.List()
To retrieve a single secret's metadata and value (used when resolving secrets at workspace creation time):
item, value, err := store.Get("my-token")
Get returns ErrSecretNotFound if no secret with the given name exists.
To remove a secret:
err := store.Remove("my-token")
Remove deletes the value from the keychain and the metadata entry from secrets.json. If the keychain entry is missing (e.g. manually deleted), the metadata is still cleaned up. ErrSecretNotFound is returned when no secret with the given name exists.
Adding a New Named Secret Type
Add an entry to pkg/secretservicesetup/secretservices.json:
{
"name": "my-service",
"hostsPatterns": ["api.my-service.com"],
"headerName": "Authorization",
"headerTemplate": "Bearer ${value}",
"envVars": ["MY_SERVICE_TOKEN"]
}
All fields:
| Field | Required | Description |
|---|
name | yes | Identifier used as --type value |
hostsPatterns | yes | List of regex patterns matched against the request host |
headerName | yes | HTTP header to set |
headerTemplate | yes | Header value template; ${value} is replaced with the secret value |
path | no | URL path prefix restriction |
envVars | no | Environment variable names to populate with the secret value |
No code changes required. Once added, the type is immediately:
- Accepted by
kdn secret create --type my-service
- Listed in
--type shell completion
- Returned by
secretservicesetup.ListAvailable()
Deriving Valid Types in Commands
Commands get valid type names from secretservicesetup.ListAvailable() rather than a hardcoded list. The built-in other type is appended separately:
import (
"github.com/openkaiden/kdn/pkg/secret"
"github.com/openkaiden/kdn/pkg/secretservicesetup"
)
registeredTypes := secretservicesetup.ListAvailable()
sort.Strings(registeredTypes)
validTypes := append(registeredTypes, secret.TypeOther)
Register shell completion from this list:
cmd.RegisterFlagCompletionFunc("type", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return validTypes, cobra.ShellCompDirectiveNoFileComp
})
Testing
Inject a fakeKeyring to avoid touching the real system keychain:
For command-level tests that bypass NewSecretCreateCmd(), populate validTypes directly on the struct:
c := &secretCreateCmd{
secretType: "github",
value: "ghp_token",
validTypes: []string{"github", secret.TypeOther},
}
For commands that call store.List(), implement a fake that satisfies the full Store interface:
type fakeListStore struct {
items []secret.ListItem
err error
}
func (f *fakeListStore) Create(_ secret.CreateParams) error { return nil }
func (f *fakeListStore) List() ([]secret.ListItem, error) { return f.items, f.err }
func (f *fakeListStore) Remove(_ string) error { return f.err }
func (f *fakeListStore) Get(name string) (secret.ListItem, string, error) {
for _, item := range f.items {
if item.Name == name {
return item, "", f.err
}
}
return secret.ListItem{}, "", f.err
}