Documentation
¶
Overview ¶
Package hokora is a client for the hokora secret management server.
A hokora server hands a machine a short-lived token in exchange for its client credential, then returns the secrets that the machine is granted. This package performs that exchange and holds the returned values in memory only; it never writes them to disk and keeps no cache.
Credentials ¶
New resolves the client credential, server address, project, and environment from three sources, in order:
- Options passed to New (WithAddress, WithCredentials, WithProject, WithEnv).
- A credentials file. Under systemd this is $CREDENTIALS_DIRECTORY/hokora, populated by LoadCredential=; the path can also be set with WithCredentialsFile. The file holds KEY=VALUE lines: HOKORA_ADDR, HOKORA_CLIENT_ID, HOKORA_CLIENT_SECRET, HOKORA_PROJECT, HOKORA_ENV.
- The same names as environment variables.
A value found earlier in this list wins over a value found later.
Security ¶
This package does not defend against an attacker who has obtained the same operating-system user as your application. Such an attacker can read the machine credential (from $CREDENTIALS_DIRECTORY or the environment) and fetch the very same secrets, or read your process memory directly. Nor does it prevent the operating system from writing process memory to disk through swap, core dumps, or kernel crash dumps. See the project's threat model.
This package never disables TLS certificate verification. To trust an internal certificate authority, pass its pool with WithRootCAs; there is no insecure-skip-verify option.
Example ¶
The most common use: New resolves the credential from the environment set by systemd's LoadCredential=, then Fetch returns every granted secret. Keep the values in memory and Zero them when done.
package main
import (
"context"
"fmt"
"log"
hokora "github.com/kan/hokora/sdk"
)
func main() {
client, err := hokora.New() // reads $CREDENTIALS_DIRECTORY/hokora, then the environment
if err != nil {
log.Fatal(err)
}
secrets, err := client.Fetch(context.Background())
if err != nil {
log.Fatal(err)
}
defer secrets.Zero()
dsn := secrets.MustGetString("DATABASE_URL")
fmt.Println(len(dsn))
}
Output:
Index ¶
- Variables
- type Client
- type Option
- func WithAddress(addr string) Option
- func WithCredentials(clientID, clientSecret string) Option
- func WithCredentialsFile(path string) Option
- func WithEnv(env string) Option
- func WithHTTPClient(client *http.Client) Option
- func WithProject(project string) Option
- func WithRootCAs(pool *x509.CertPool) Option
- type Secrets
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrMissingConfig indicates that a required setting could not be // resolved from options, the credentials file, or the environment. ErrMissingConfig = errors.New("hokora: missing configuration") ErrUnauthorized = errors.New("hokora: invalid credentials") // ErrForbidden indicates that the machine is not granted the requested // project and environment. ErrForbidden = errors.New("hokora: forbidden") // ErrSealed indicates that the server is sealed and cannot serve secrets. ErrSealed = errors.New("hokora: server is sealed") )
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client fetches secrets from a hokora server. It is safe for concurrent use.
func New ¶
New creates a Client, resolving settings as described in the package documentation.
Example (Options) ¶
Configure the client explicitly instead of relying on the environment.
package main
import (
"log"
"os"
hokora "github.com/kan/hokora/sdk"
)
func main() {
client, err := hokora.New(
hokora.WithAddress("https://hokora.example.com:9443"),
hokora.WithCredentials("app-prod", os.Getenv("APP_HOKORA_SECRET")),
hokora.WithProject("myapp"),
hokora.WithEnv("prod"),
)
if err != nil {
log.Fatal(err)
}
_ = client
}
Output:
func (*Client) Fetch ¶
Fetch retrieves every secret the machine is granted for the configured project and environment.
Each call authenticates and fetches; nothing is cached between calls. The caller owns the returned Secrets and should call Zero when finished.
Example (Refresh) ¶
Fetch does not cache; call it again to pick up rotated values. Give each call a bounded context.
package main
import (
"context"
"fmt"
"log"
"time"
hokora "github.com/kan/hokora/sdk"
)
func main() {
client, err := hokora.New()
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
secrets, err := client.Fetch(ctx)
if err != nil {
log.Print(err)
return
}
defer secrets.Zero()
fmt.Println(secrets.Len())
}
Output:
func (*Client) FetchKey ¶
FetchKey retrieves a single secret by key for the configured project and environment. It hits the server's single-key endpoint, so only that one key is read and audited — unlike Fetch, which reads (and audits) every granted key. Prefer FetchKey when you need one value.
The returned Secrets holds just that key. As with Fetch, nothing is cached; the caller owns the result and should call Zero when finished. A key that does not exist is reported as ErrForbidden, indistinguishable from a missing grant (the server does not reveal which keys exist).
Example ¶
FetchKey retrieves a single secret. Prefer it over Fetch when you need one value: only that key is read and audited on the server. A key that does not exist is reported as ErrForbidden, indistinguishable from a missing grant.
package main
import (
"context"
"fmt"
"log"
hokora "github.com/kan/hokora/sdk"
)
func main() {
client, err := hokora.New()
if err != nil {
log.Fatal(err)
}
secrets, err := client.FetchKey(context.Background(), "DATABASE_URL")
if err != nil {
log.Fatal(err)
}
defer secrets.Zero()
if v, ok := secrets.Get("DATABASE_URL"); ok {
fmt.Println(len(v))
}
}
Output:
type Option ¶
type Option func(*config)
Option configures a Client. See New for the resolution order.
func WithAddress ¶
WithAddress sets the base URL of the hokora server, for example "https://hokora.example.com:9443".
func WithCredentials ¶
WithCredentials sets the machine credential explicitly.
func WithCredentialsFile ¶
WithCredentialsFile reads settings from a KEY=VALUE file. When unset, New falls back to $CREDENTIALS_DIRECTORY/hokora if that variable is present.
func WithHTTPClient ¶
WithHTTPClient uses a caller-provided HTTP client instead of the default.
The provided client's TLS configuration is used as-is; WithRootCAs is ignored when this option is set.
func WithProject ¶
WithProject sets the project slug to fetch.
func WithRootCAs ¶
WithRootCAs verifies the server against the given certificate pool, replacing the system roots. Use it when the server presents a certificate from an internal CA. To trust the internal CA in addition to the public roots, seed the pool from x509.SystemCertPool() before adding the CA; a pool cannot be merged with the system roots after the fact.
There is no option to skip verification.
Example ¶
To trust a server certificate issued by an internal CA, load the CA into a pool and pass it with WithRootCAs. This replaces the system roots; seed the pool from x509.SystemCertPool first if you need both. There is no option to skip verification.
package main
import (
"crypto/x509"
"log"
"os"
hokora "github.com/kan/hokora/sdk"
)
func main() {
pem, err := os.ReadFile("/etc/hokora/internal-ca.pem")
if err != nil {
log.Fatal(err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pem) {
log.Fatal("no certificates found in the CA file")
}
client, err := hokora.New(hokora.WithRootCAs(pool))
if err != nil {
log.Fatal(err)
}
_ = client
}
Output:
type Secrets ¶
type Secrets struct {
// contains filtered or unexported fields
}
Secrets holds fetched secret values in memory.
Values are stored as byte slices so that Zero can overwrite them. Secrets is not safe for concurrent modification, but concurrent reads are fine once fetching has returned it.
func (*Secrets) Get ¶
Get returns the value for key.
The returned slice aliases the internal storage; do not modify it, and do not retain it past a call to Zero. ok is false when the key is absent.
func (*Secrets) GetString ¶
GetString returns the value for key as a string.
Go strings are immutable, so a value obtained through this method cannot be overwritten by Zero and may outlive it. Prefer Get when the value's lifetime in memory matters.
func (*Secrets) MustGetString ¶
MustGetString returns the value for key and panics if it is absent.
It is meant for application startup, where a missing secret should stop the program immediately rather than surface later as a nil value.
func (*Secrets) Zero ¶
func (s *Secrets) Zero()
Zero overwrites the stored secret values and drops them.
This is best-effort. Values already returned by GetString cannot be zeroed because Go strings are immutable, and the Go runtime may have retained copies made while garbage collecting. Zero also cannot undo a value that your program has copied elsewhere. See the package's Security section.
After Zero, Get and GetString report every key as absent.