Skip to content

Configuration and options

localca is configured by two things: a typed Config struct that describes the certificate authority, and a set of functional Options that inject the runtime environment. There is no config file, no environment variable and no CLI flag — a consuming tool decodes its own configuration into Config itself.

Config fields at a glance

Field Type Required Default when zero Set it when
DataDir string yes — (construction fails) always
AppName string no "local" you want the root recognisable in a trust-store UI
Organization string no the resolved AppName your org name differs from the app name
RootTTL time.Duration no 3650 * 24h (10 × 365 days) rarely
LeafTTL time.Duration no 90 * 24h rarely — see the seven-day floor below

Defaults are applied by value at construction. New and EnsureServed copy the Config you pass, so mutating your struct afterwards changes nothing.

DataDir — where everything is stored

The directory holding the root key and certificate, the cached leaf, and the trust-install marker. It is the only required field.

  • Leaving it empty makes New return localca: Config.DataDir is required, and EnsureServed return the same error before it does anything else.
  • The directory is created on first write with mode 0700. Parent directories are created too.
  • It must be a stable per-machine path. Pointing it at a temporary directory means a new root is minted — and a new trust-store entry installed — every run, which is how you end up with dozens of stale development CAs in your system store.
  • Two Authority instances sharing a DataDir share one root and one cached leaf. See one leaf per data directory.

The exact files written are listed in data directory layout.

AppName — what the root is called

Seeds the root certificate's Common Name, which is what a human sees in Keychain Access, certmgr.msc, or Firefox's certificate manager. The CN is built as:

<AppName> local CA (<username>@<hostname>)
  • Default "local" produces local local CA (me@laptop) — the repetition is real, so set AppName to your tool's name.
  • The username comes from os/user.Current(); if that fails, or returns an empty username, the literal unknown is used.
  • The hostname comes from os.Hostname(); if that fails, or returns empty, the literal localhost is used.
  • AppName also prefixes the trust-store entry name as <AppName>-localca-<serial>, which is what you search for when removing an entry by hand.
  • Nothing is validated or escaped. A name with slashes or newlines in it produces a correspondingly odd trust-store entry.

Changing AppName on an existing data directory does not re-mint or rename anything: the stored root is loaded as-is and keeps its original CN. Only a root minted after the change carries the new name.

Organization — the root's O field

Seeds the root subject's Organization. It defaults to the resolved AppName, so with both fields empty the root is O=local. It has no effect on trust decisions; it is metadata a human reads in a certificate viewer.

RootTTL — how long the local CA lasts

Validity of the root certificate, measured from one hour before the mint time (see clock skew). Zero means ten years.

What happens when it is wrong:

  • Shorter than LeafTTL — every leaf is clamped to the root's expiry and a WARN is logged on every mint. Serving still works; the leaves are just shorter than you asked for.
  • Already elapsed (a negative duration, or a root that has genuinely aged out) — leaves are minted already expired and clients reject them. localca does not detect this and does not re-mint the root; you must Uninstall with Purge() and provision again.
  • Very long — accepted. Nothing enforces the 825-day cap browsers apply to leaf certificates, because that cap does not apply to roots you have trusted yourself.

RootTTL is read only when a root is minted. Changing it later does not extend an existing root.

LeafTTL — how long each server certificate lasts

Validity of each issued leaf. Zero means 90 days.

  • A cached leaf is re-minted once it is within seven days of expiring. That window is fixed and not configurable.
  • Therefore a LeafTTL of seven days or less defeats the cache entirely: the freshly minted leaf is already inside the renewal window, so every EnsureServed call mints and rewrites a new certificate and key. Nothing breaks, but you are doing key generation and two file writes on every call. Keep LeafTTL comfortably above seven days.
  • A leaf is never issued past the root's NotAfter; see RootTTL.
  • LeafTTL is read at every mint, so lowering it takes effect on the next renewal.

Clock skew and NotBefore

Both roots and leaves are backdated by one hour: NotBefore is set to one hour before the mint time. This tolerates small clock differences between the machine that issued the certificate and a client connecting to it. It is a fixed constant, not configurable.

Options: injecting the filesystem, logger, trust store and clock

Options inject the runtime environment. Every one is optional, and every one ignores a nil argument rather than panicking — passing WithLogger(nil) leaves logging silent rather than failing.

WithFS(fs afero.Fs)

Replaces the filesystem used for localca's own storage. Default afero.NewOsFs().

This does not sandbox the trust store. The default trust-store backend shells out to real OS commands (sudo, certutil, security, update-ca-certificates) and writes its temporary certificate through the real os package. Passing an in-memory filesystem gives you an in-memory root and leaf while the install still touches the real machine. To keep a test off the host entirely, combine WithFS with WithTrustStore.

WithLogger(l *slog.Logger)

Attaches a structured logger. The default is slog.New(slog.DiscardHandler), so localca is silent unless you pass a logger — including the warning that your root CA is about to expire. See errors and log output for every line it can emit.

WithTrustStore(ts TrustStore)

Replaces the whole cross-OS trust-store backend. This is the seam that makes the certificate logic testable without touching a real store, and the way to run EnsureServed in a test: supply an implementation whose State reports System: true and nothing is installed anywhere.

When it is not supplied, the default backend is constructed after defaults are applied, so it sees the resolved DataDir and AppName.

WithClock(now func() time.Time)

Replaces the time source used for NotBefore/NotAfter and for the cached-leaf expiry check. Intended for deterministic tests. It does not affect the trust store, which uses real system time.

Purge()

An UninstallOption, not an Option. Passing it to Authority.Uninstall deletes the stored root certificate and key after removing the root from the trust stores. Without it, Uninstall removes the trust but leaves the key material in place so a later Install re-uses the same root.

Purge() deletes the root files only — it does not delete the cached leaf. See what Purge leaves behind.

Configuring from a tool's own config file

Config is a plain struct with no tags and no framework coupling, which is deliberate: it must not force a configuration library on consumers. Decode your own configuration section into it in an adapter:

// A tool's own config type, whatever shape it already uses.
type tlsSection struct {
    DataDir string `yaml:"data_dir"`
    LeafDays int   `yaml:"leaf_days"`
}

func (s tlsSection) localCA(appName string) localca.Config {
    return localca.Config{
        DataDir: s.DataDir,
        AppName: appName,
        LeafTTL: time.Duration(s.LeafDays) * 24 * time.Hour,
    }
}

Leave a field at its zero value to take localca's default; there is no separate "unset" sentinel.