Errors and log output¶
The sentinel errors you should handle¶
ErrElevationUnavailable¶
var ErrElevationUnavailable = errors.New(
"localca: administrator privileges are required to install into the system trust " +
"store, but no elevation is available (not root, and no interactive terminal to prompt)",
)
Returned when a system-store install needs administrator privileges that cannot be
obtained: the process is not root, and either sudo is missing from PATH or standard
input is not an interactive terminal.
It is the one error a consuming tool is expected to branch on, because it is not a fault.
It is the environment telling you to take a different path. Match it with errors.Is; it
survives the wrapping that Install applies:
if _, err := authority.Install(ctx); errors.Is(err, localca.ErrElevationUnavailable) {
// Fall back: NSS only, an untrusted self-signed certificate, or tell the
// user to run the install once from a terminal.
}
ErrRequestRefused¶
Every refusal from Private.SignClient wraps it: a client name, a CSR or a lifetime that
the caller or the requesting machine got wrong. Its message says which, for example
localca: request refused: the CSR's subject must be exactly CN="relay-1", got "CN=relay-2".
Branch on it to tell a bad request (answer the machine with a refusal) from a failure of the
CA itself (report it):
cert, err := ca.SignClient(ctx, csr, localca.ClientRequest{Name: machine})
if errors.Is(err, localca.ErrRequestRefused) {
// The request was wrong; tell the machine. Nothing was signed or written.
}
Everything else localca returns is a fault to report, not a condition to branch on.
Every error localca produces itself¶
All are created with gitlab.com/phpboyscout/go/errors, so they carry a stack trace and
unwrap normally. Errors from the standard library or the trust-store backend are wrapped
with a short context prefix and passed through.
| Message | Returned by | What it means |
|---|---|---|
localca: Config.DataDir is required |
New, NewPrivate, EnsureServed |
Config.DataDir was empty. |
localca: EnsureServed needs at least one host |
EnsureServed |
The hosts slice was empty. |
localca: no hosts for leaf certificate |
Authority.Leaf, Private.Leaf |
Leaf was called with no host arguments. |
localca: root CA expired at <NotAfter>; renew it with Uninstall(Purge()) and provision again |
Authority.Leaf, EnsureServed |
The root's NotAfter has passed, so no leaf it signs could be valid. Nothing is written. See the expiry warning for how to renew. |
localca: root CA expired at <NotAfter>; renew it with a fresh root: stop the service, delete privateCA.pem and privateCA-key.pem, and enrol every machine again |
Private.Leaf, Private.SignClient |
The same, for a Private CA, whose root is renewed by hand. |
localca: <DataDir> holds rootCA.pem, an installable root; a Private CA needs a DataDir of its own |
NewPrivate |
The directory already belongs to an Authority. |
localca: Config.ClientTTL <d> exceeds MaxClientTTL <d> |
NewPrivate |
The default client lifetime is longer than the maximum allowed. |
localca: no root CA provisioned (call Install or EnsureServed first) |
Authority.Installed, Authority.Leaf |
No root has been minted yet. Neither call mints one. |
localca: root cert present but key missing |
any call that loads the root | rootCA.pem exists but rootCA-key.pem does not. See recovering from a half-written root. |
localca: stored root cert is not valid PEM |
any call that loads the root | rootCA.pem is not a PEM CERTIFICATE block. |
localca: stored root key is not valid PEM |
any call that loads the root | rootCA-key.pem is not a PEM EC PRIVATE KEY block. |
A leaf that fails to parse is not an error: it is treated as an empty cache and re-minted.
Wrapped errors and their prefixes¶
These prefixes tell you which step failed. The wrapped cause carries the detail.
| Prefix | Step |
|---|---|
creating data dir |
creating DataDir |
reading <file>, writing <file>, flushing <file>, setting mode on <file>, renaming <file> into place, creating temp file for <file>, removing <file> |
the atomic file write, at the named stage |
generating root key, creating root certificate, parsing root certificate, marshalling root key |
minting the root |
generating leaf key, creating leaf certificate, marshalling leaf key |
minting a leaf |
generating certificate serial |
drawing the random serial, reached when minting either the root or a leaf |
parsing stored root certificate, parsing stored root key |
loading a stored root whose PEM decoded but whose contents did not parse |
assembling leaf key pair |
turning the minted PEM into a tls.Certificate |
installing root into trust store |
the trust-store install |
removing root from trust store |
the trust-store uninstall |
encoding trust marker, writing trust marker |
updating trust-install.json |
pre-authorising sudo for the trust-store install |
the sudo -v pre-warm failed or was cancelled, typically a wrong password, or the user pressing Ctrl-C at the prompt |
installing root into trust store appears twice in a failed install: once from the
trust-store backend and once from Authority.Install wrapping it. That is cosmetic;
errors.Is and the underlying cause are unaffected.
What localca logs, and when¶
localca logs through an injected *slog.Logger. The default is a discard handler, so
nothing is logged unless you pass WithLogger.
That includes the expiry warning below, which is the only advance notice you get that your
local CA is running out.
There are exactly two lines.
INFO: a root is about to be installed¶
installing a local development CA into the trust store so this tool can serve HTTPS
locally; reverse with Uninstall
Attribute: common_name, the root's Common Name.
Emitted by Install (including the Install that EnsureServed performs) only on the
call that mints the root, immediately before the trust-store install and therefore
immediately before any password prompt. It exists so a user who is suddenly asked for a
password can see why. If you attach no logger, the prompt appears with no explanation.
Loading an existing root does not log it, so this line appears once in the life of a data directory.
WARN: the root CA is expiring¶
root CA expiring soon; leaf lifetime clamped to the root's. Renew it with
Uninstall(Purge()) and provision again
Attributes: common_name, and root_not_after, the root's expiry timestamp.
Emitted whenever a leaf has to be shortened because the requested LeafTTL would have run
past the root's expiry. In practice that means the root has less than one leaf lifetime
left: by default, less than 90 days. It repeats on every mint from then on, until the
root expires; after that minting fails with the root CA expired at error instead.
The fix is to renew the root: Uninstall with Purge(), delete the cached leaf files,
then provision again. See what Purge leaves behind.
A Private CA logs the same line, with the same attributes, whenever one of its server
leaves or client certificates is shortened. For a client certificate the window is its
lifetime: ClientTTL, 30 days by default, or the lifetime the request asked for. Its advice
replaces Uninstall(Purge()):
root CA expiring soon; leaf lifetime clamped to the root's. Renew it with a fresh
root: stop the service, delete privateCA.pem and privateCA-key.pem, and enrol every
machine again
What is deliberately not logged¶
- The private key, and any part of it.
- The certificate contents. Only the Common Name and the root's expiry appear in log attributes.
- The trust-store commands and their output.
sudo,certutilandsecuritywrite their own messages to the terminal directly;localcadoes not capture or re-emit them, which is why a failing install can print output that does not appear in your structured logs.