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
Newreturnlocalca: Config.DataDir is required, andEnsureServedreturn 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
Authorityinstances sharing aDataDirshare 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:
- Default
"local"produceslocal local CA (me@laptop)— the repetition is real, so setAppNameto your tool's name. - The username comes from
os/user.Current(); if that fails, or returns an empty username, the literalunknownis used. - The hostname comes from
os.Hostname(); if that fails, or returns empty, the literallocalhostis used. AppNamealso 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 aWARNis 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.
localcadoes not detect this and does not re-mint the root; you mustUninstallwithPurge()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
LeafTTLof seven days or less defeats the cache entirely: the freshly minted leaf is already inside the renewal window, so everyEnsureServedcall mints and rewrites a new certificate and key. Nothing breaks, but you are doing key generation and two file writes on every call. KeepLeafTTLcomfortably above seven days. - A leaf is never issued past the root's
NotAfter; seeRootTTL. LeafTTLis 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.