Documentation
¶
Overview ¶
Package lancert holds the publicly trusted certificate of a homelab's LAN name under its kombify.me base (homelab identity target §5, §14 decision 3): one certificate for lan.<base>.kombify.me only, with which the Authority's LAN enrollment listener serves the LAN name the installed Hub PWA opens from home.
The node generates the certificate key and its ACME account key and keeps both in Owner custody; it runs its own ACME client against a second issuer (ZeroSSL or Google Trust Services with external account binding, until kombify.me is on the Public Suffix List). kombify Me publishes only the DNS-01 value and, as the LAN name's one DNS-only record, the node's private site address. Neither key, the CSR's key nor the EAB key is printed, logged or written to a receipt.
Index ¶
- Constants
- Variables
- func AuthoritativeTXT(ctx context.Context, name, value string) error
- func Covers(workspace, host string, now time.Time) bool
- func CoversProfile(workspace string, profile Profile, host string, now time.Time) bool
- func CustodyDir(workspace string) string
- func CustodyDirOf(workspace string, p Profile) string
- func LANName(domain string) string
- func LiveDir(workspace string) string
- func LiveDirOf(workspace string, p Profile) string
- func NeedsRenewal(workspace, domain string, now time.Time) bool
- func NeedsRenewalProfile(workspace string, profile Profile, domain string, now time.Time) bool
- func PublishAddress(ctx context.Context, workspace, domain string, publisher Publisher) (netip.Addr, error)
- func Remove(workspace string) error
- func RemoveConfined(dir string, names ...string) error
- func RemoveProfile(workspace string, profile Profile) error
- func Renew(ctx context.Context, workspace, domain string, publisher Publisher, ...) (bool, error)
- func RenewProfile(ctx context.Context, workspace string, profile Profile, domain string, ...) (bool, error)
- func SiteAddress(workspace string) (netip.Addr, error)
- func WriteConfined(dir, name string, raw []byte, mode os.FileMode, uid, gid int) error
- type Cloudflare
- type Issuer
- type Profile
- type Publisher
- type Source
- type State
Constants ¶
const (
// LANLabel is the label of the LAN name under the homelab's domain.
LANLabel = "lan"
)
Variables ¶
var ErrAddressNotPublished = errors.New("lancert: the LAN address is not published")
ErrAddressNotPublished reports that kombify Me did not publish the LAN name's address (or the node had none to publish). Nothing was ordered: a caller may retry soon without touching the ACME issuer.
var Issuers = map[string]string{
"zerossl": "https://acme.zerossl.com/v2/DV90",
"gts": "https://dv.acme-v02.api.pki.goog/directory",
"letsencrypt-staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
}
Issuer directories. ZeroSSL and Google Trust Services need external account binding. Let's Encrypt production is not offered: its 50 certificates per week for the registered domain kombify.me are shared by every homelab until the Public Suffix List entry takes effect (target §14 decision 3). Let's Encrypt staging serves journeys only; its chain is not trusted.
var LAN = Profile{Custody: custodyDirName, Names: func(domain string) []string { return []string{LANName(domain)} }, PublishAddress: true, /* contains filtered or unexported fields */}
LAN is the lan.<domain> profile (the zero Issuer profile).
var Public = Profile{ Custody: "public-tls", Names: func(domain string) []string { return []string{"*." + domain, domain} }, // contains filtered or unexported fields }
Public is the public-names profile.
Functions ¶
func AuthoritativeTXT ¶
AuthoritativeTXT waits until every authoritative name server of the zone answers value for name, querying them directly: a shared recursive resolver asked too early caches the missing answer.
func Covers ¶
Covers reports whether the installed key pair matches and its certificate is valid for host for at least another hour.
func CoversProfile ¶
CoversProfile is Covers for a profile.
func CustodyDir ¶
CustodyDir is the Owner custody directory of the LAN certificate.
func CustodyDirOf ¶
func NeedsRenewal ¶
NeedsRenewal reports a missing certificate, one that does not cover the domain's names, and one within 30 days of expiry.
func NeedsRenewalProfile ¶
NeedsRenewalProfile is NeedsRenewal for a profile.
func PublishAddress ¶
func PublishAddress(ctx context.Context, workspace, domain string, publisher Publisher) (netip.Addr, error)
PublishAddress publishes the node site address for lan.<domain> through kombify Me before any ACME order: without that record the base's proxied wildcard answers the LAN name with Cloudflare addresses. A router that drops private answers (DNS rebinding protection) still leaves the Hub on the away flow. Every failure wraps ErrAddressNotPublished.
func Remove ¶
Remove deletes the certificate, its key and the ACME account key under the custody lock. The live directory itself stays: the Basement core mounts it.
func RemoveConfined ¶
RemoveConfined removes the named files under dir, confined to dir; a missing file or directory is no error.
func RemoveProfile ¶
RemoveProfile is Remove for a profile.
func Renew ¶
func Renew(ctx context.Context, workspace, domain string, publisher Publisher, keyUID, keyGID int) (bool, error)
Renew renews an issued LAN certificate within 30 days of its expiry with the account in custody (no external account binding is needed again), and otherwise republishes the LAN name's address: Me removes that record with every tunnel teardown, and the site address can move. A node that never issued one has nothing to renew or publish.
func RenewProfile ¶
func RenewProfile(ctx context.Context, workspace string, profile Profile, domain string, publisher Publisher, keyUID, keyGID int) (bool, error)
RenewProfile is Renew for a profile.
func SiteAddress ¶
SiteAddress is the address lan.<domain> must resolve to: the node site address the owner-signed LAN resolver custody binds, which the LAN enrollment listener (9443) is published on and announces. Only an RFC 1918 or IPv6 ULA address is answered (narrower than Me, which also admits 100.64.0.0/10); a public, loopback, link-local, IPv4-mapped or zoned address never leaves the node.
Types ¶
type Cloudflare ¶
type Cloudflare struct {
Token string
// API overrides the Cloudflare API base (tests).
API string
Client *http.Client
// contains filtered or unexported fields
}
Cloudflare answers DNS-01 for an own domain in the owner's Cloudflare zone with the owner's API token (a token limited to that zone with Zone:Read, which zoneOf needs to look the zone up by name, and DNS:Edit). It publishes only the challenge TXT record under _acme-challenge.<name> and removes exactly the record it created; it never touches an address record, and the token is never logged or returned in an error.
func (*Cloudflare) RemoveChallenge ¶
func (c *Cloudflare) RemoveChallenge(ctx context.Context, identifier, value string) error
RemoveChallenge deletes the record SetChallenge created for the value.
func (*Cloudflare) SetChallenge ¶
func (c *Cloudflare) SetChallenge(ctx context.Context, identifier, value string) error
SetChallenge publishes one DNS-01 value as a TXT record.
func (*Cloudflare) SetLANAddress ¶
SetLANAddress is not offered: an own domain's address records are the owner's.
type Issuer ¶
type Issuer struct {
// Profile is the certificate to obtain; the zero value is the LAN name.
Profile Profile
Workspace string
Domain string
// Directory is the ACME directory URL of the issuer.
Directory string
// EAB is required for a new account at ZeroSSL and Google Trust Services.
EAB *acme.ExternalAccountBinding
// Contact is the optional ACME account contact address; Google Trust
// Services and ZeroSSL refuse an account without one.
Contact string
Publisher Publisher
// KeyUID and KeyGID own the certificate key file: the core server's
// account, which serves the LAN enrollment listener.
KeyUID, KeyGID int
// Propagated waits until the authoritative servers answer the value.
Propagated func(ctx context.Context, name, value string) error
}
Issuer obtains the LAN certificate.
func (Issuer) Issue ¶
Issue publishes the LAN name's address, registers or reuses the node's ACME account, proves control of the LAN name lan.<domain> (and nothing else: the key lives in the core, so it must not be able to impersonate the identity origin or any service) through the publisher and installs the certificate. Nothing is ordered unless Me read the address back. One issuance runs at a time.
type Profile ¶
type Profile struct {
// Custody is the directory under .stackkit/custody that holds the ACME
// account and the live directory.
Custody string
// Names are the certificate's DNS names for a domain.
Names func(domain string) []string
// PublishAddress publishes the node's private site address for the LAN name.
PublishAddress bool
// contains filtered or unexported fields
}
Profile selects which certificate an Issuer obtains and where it lives. The LAN profile is the lan.<domain> certificate the Authority's LAN listener serves. The Public profile is the certificate of a public identity origin's router: *.<domain> and <domain> (id.<name>, base.<name>, ...), obtained by DNS-01 through kombify Me (a kombify.me name) or the owner's DNS provider (an own domain); the Basement core router mounts its live directory read-only.
type Publisher ¶
type Publisher interface {
SetChallenge(ctx context.Context, identifier, value string) error
RemoveChallenge(ctx context.Context, identifier, value string) error
SetLANAddress(ctx context.Context, name string, address netip.Addr) error
}
Publisher is kombify Me's side of the LAN name: it publishes and removes one DNS-01 value, and publishes the node's private address as the DNS-only record of the LAN name, answering nil only after Me's read-back shows that name, DNS-only, with exactly that address.
type Source ¶
type Source struct {
Workspace string
// contains filtered or unexported fields
}
Source serves the installed certificate to a TLS listener, reloading it when the file changes.
func (*Source) Certificate ¶
func (s *Source) Certificate() *tls.Certificate
Certificate answers the installed key pair, or nil when none is installed.
type State ¶
type State struct {
Names []string `json:"names"`
Issuer string `json:"issuer"`
NotBefore string `json:"notBefore"`
NotAfter string `json:"notAfter"`
SerialHex string `json:"serialHex"`
CertificateSHA256 string `json:"certificateSha256"`
}
State is the public identity of the installed certificate.
func Load ¶
func Load(workspace string) (State, *x509.Certificate, error)
Load parses the installed certificate; os.ErrNotExist when none is.
func LoadPublic ¶
func LoadPublic(workspace string) (State, *x509.Certificate, error)
LoadPublic parses the installed public-names certificate.