hokora

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

hokora SDK

Go client for a hokora secret-management server. It exchanges a machine credential for a short-lived token and returns the granted secrets in memory — it never writes them to disk and keeps no cache.

go get github.com/kan/hokora/sdk

It is a separate module from the server, so importing it adds exactly one module to your build list and nothing else: it has no dependencies outside the standard library, and it does not drag in the server's SQLite driver or its Go version requirement. Minimum Go: 1.24. Both properties are enforced by tests (deps_test.go), not just documented.

Full API reference: pkg.go.dev/github.com/kan/hokora/sdk

import hokora "github.com/kan/hokora/sdk"

client, err := hokora.New()          // resolves $CREDENTIALS_DIRECTORY/hokora, then the env
secrets, err := client.Fetch(ctx)
dsn := secrets.MustGetString("DATABASE_URL")
defer secrets.Zero()                 // best effort; see the caveats below

Configuration is resolved in order: options → credentials file ($CREDENTIALS_DIRECTORY/hokora under systemd, or WithCredentialsFile) → the HOKORA_ADDR / HOKORA_CLIENT_ID / HOKORA_CLIENT_SECRET / HOKORA_PROJECT / HOKORA_ENV environment variables.

This package depends on the Go standard library only.

Security

This SDK does not defend against an attacker who has your application's OS user — they can read the same credential and fetch the same secrets, or read your process memory. It cannot stop the OS from writing memory to disk via swap, core dumps, or kernel crash dumps, and Zero is best-effort (values obtained through GetString are immutable Go strings and cannot be overwritten). It never disables TLS verification. See the project's threat model.

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:

  1. Options passed to New (WithAddress, WithCredentials, WithProject, WithEnv).
  2. 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.
  3. 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))
}

Index

Examples

Constants

This section is empty.

Variables

View Source
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 indicates that the server rejected the credential.
	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

func New(opts ...Option) (*Client, error)

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
}

func (*Client) Fetch

func (c *Client) Fetch(ctx context.Context) (*Secrets, error)

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())
}

func (*Client) FetchKey

func (c *Client) FetchKey(ctx context.Context, key string) (*Secrets, error)

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))
	}
}

type Option

type Option func(*config)

Option configures a Client. See New for the resolution order.

func WithAddress

func WithAddress(addr string) Option

WithAddress sets the base URL of the hokora server, for example "https://hokora.example.com:9443".

func WithCredentials

func WithCredentials(clientID, clientSecret string) Option

WithCredentials sets the machine credential explicitly.

func WithCredentialsFile

func WithCredentialsFile(path string) Option

WithCredentialsFile reads settings from a KEY=VALUE file. When unset, New falls back to $CREDENTIALS_DIRECTORY/hokora if that variable is present.

func WithEnv

func WithEnv(env string) Option

WithEnv sets the environment slug to fetch.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

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

func WithProject(project string) Option

WithProject sets the project slug to fetch.

func WithRootCAs

func WithRootCAs(pool *x509.CertPool) Option

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
}

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

func (s *Secrets) Get(key string) (value []byte, ok bool)

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

func (s *Secrets) GetString(key string) (value string, ok bool)

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) Keys

func (s *Secrets) Keys() []string

Keys returns the names of the fetched secrets. The order is unspecified.

func (*Secrets) Len

func (s *Secrets) Len() int

Len reports how many secrets were fetched.

func (*Secrets) MustGetString

func (s *Secrets) MustGetString(key string) string

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL