hive-tls: no hive leaf or hive identity on a gateway host without a hive
A self-signed gateway on a host with deploy.hive-controller.enable off (a dedicated swarm-services host) needed the hive leaf for its catch-all, and that leaf now comes only from hive-tls-resign with a per-hive store identity. hive-tls-resign and its timer are now defined only where a hive runs; elsewhere hive-gateway-self-signed-cert installs the swarm-services leaf as the default cert, and the gateway waits for swarm-services-cert. Also corrects the hive-gateway comments and option text that still said the leaf is hive-CA-signed. Refs #4588
This commit is contained in:
parent
fc33a62bf8
commit
75a7dccee6
6 changed files with 90 additions and 24 deletions
|
|
@ -63,6 +63,12 @@ lands, and then the unit starts nginx.
|
||||||
nginx serves the leaf alone, without its issuer: the issuer is the
|
nginx serves the leaf alone, without its issuer: the issuer is the
|
||||||
self-signed root a consumer already holds in its trust bundle.
|
self-signed root a consumer already holds in its trust bundle.
|
||||||
|
|
||||||
|
**A gateway host that runs no hive** (`deploy.hive-controller.enable` off,
|
||||||
|
such as a dedicated swarm-services host) has no hive leaf, no
|
||||||
|
`hive-tls-resign.service` and needs no hive identity. Its gateway serves the
|
||||||
|
swarm-services leaf as its default certificate too, and waits for
|
||||||
|
`swarm-services-cert.service` at start.
|
||||||
|
|
||||||
## The trust bundle
|
## The trust bundle
|
||||||
|
|
||||||
`hive-tls-ca.service` writes `<deploy.hive-controller.tls.stateDir>/trust-bundle.pem`,
|
`hive-tls-ca.service` writes `<deploy.hive-controller.tls.stateDir>/trust-bundle.pem`,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ let
|
||||||
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
|
swarmServiceDomains = config.services.hyperhive.swarm.serviceDomains;
|
||||||
matrixCfg = config.services.hyperhive.swarm.matrix;
|
matrixCfg = config.services.hyperhive.swarm.matrix;
|
||||||
matrixDeployCfg = config.services.hyperhive.deploy.matrix;
|
matrixDeployCfg = config.services.hyperhive.deploy.matrix;
|
||||||
|
tlsStateDir = config.services.hyperhive.deploy.hive-controller.tls.stateDir;
|
||||||
|
# The hive leaf exists only where a hive runs; ../hive-tls.nix issues it
|
||||||
|
# under that same condition.
|
||||||
|
runsHive = config.services.hyperhive.deploy.hive-controller.enable;
|
||||||
|
|
||||||
# Every vhost claiming `default_server`, ours and the operator's
|
# Every vhost claiming `default_server`, ours and the operator's
|
||||||
# alike. Computed once so the assertion below and the message it
|
# alike. Computed once so the assertion below and the message it
|
||||||
|
|
@ -45,7 +49,7 @@ let
|
||||||
|
|
||||||
# Self-signed TLS is the implicit floor: when neither an operator cert
|
# Self-signed TLS is the implicit floor: when neither an operator cert
|
||||||
# (`tls.certDir`) nor ACME (`tls.acme.enable`) is configured, the gateway
|
# (`tls.certDir`) nor ACME (`tls.acme.enable`) is configured, the gateway
|
||||||
# generates + serves a hive-CA-signed leaf (see hive-tls.nix). There is no
|
# serves a leaf the secret store issues (see hive-tls.nix). There is no
|
||||||
# explicit toggle and no http-only mode — matrix discovery requires https,
|
# explicit toggle and no http-only mode — matrix discovery requires https,
|
||||||
# so the gateway always terminates TLS.
|
# so the gateway always terminates TLS.
|
||||||
# `cfg.useSelfSigned` (options.nix) is the derived single source of truth.
|
# `cfg.useSelfSigned` (options.nix) is the derived single source of truth.
|
||||||
|
|
@ -57,15 +61,16 @@ let
|
||||||
# certs for no gain.
|
# certs for no gain.
|
||||||
tlsDir = "/var/lib/hive-gateway/tls";
|
tlsDir = "/var/lib/hive-gateway/tls";
|
||||||
# TLS cert + key paths.
|
# TLS cert + key paths.
|
||||||
# - self-signed (default): the hive-CA-signed leaf, imported into the
|
# - self-signed (default): the hive leaf the store issues (the
|
||||||
# state dir by `hive-gateway-self-signed-cert` below.
|
# swarm-services leaf on a host with no hive), imported into the state
|
||||||
|
# dir by `hive-gateway-self-signed-cert` below.
|
||||||
# - tls.certDir set: the operator's own cert dir, read directly.
|
# - tls.certDir set: the operator's own cert dir, read directly.
|
||||||
tlsCert =
|
tlsCert =
|
||||||
if cfg.tls.certDir != null then "${cfg.tls.certDir}/${cfg.tls.certName}" else "${tlsDir}/cert.pem";
|
if cfg.tls.certDir != null then "${cfg.tls.certDir}/${cfg.tls.certName}" else "${tlsDir}/cert.pem";
|
||||||
tlsKey =
|
tlsKey =
|
||||||
if cfg.tls.certDir != null then "${cfg.tls.certDir}/${cfg.tls.keyName}" else "${tlsDir}/key.pem";
|
if cfg.tls.certDir != null then "${cfg.tls.certDir}/${cfg.tls.keyName}" else "${tlsDir}/key.pem";
|
||||||
# The swarm-services pair, used only by the vhosts whose names this
|
# The swarm-services pair, used only by the vhosts whose names the
|
||||||
# hive's CA cannot sign. Self-signed mode only: with an operator cert
|
# hive leaf cannot carry. Self-signed mode only: with an operator cert
|
||||||
# or ACME the operator owns every name and there is no second issuer.
|
# or ACME the operator owns every name and there is no second issuer.
|
||||||
svcCert = "${tlsDir}/swarm-services.pem";
|
svcCert = "${tlsDir}/swarm-services.pem";
|
||||||
svcKey = "${tlsDir}/swarm-services-key.pem";
|
svcKey = "${tlsDir}/swarm-services-key.pem";
|
||||||
|
|
@ -244,7 +249,7 @@ in
|
||||||
defaults.email = cfg.tls.acme.email;
|
defaults.email = cfg.tls.acme.email;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Import the hive-CA leaf into nginx's state dir before nginx starts.
|
# Import the gateway leaves into nginx's state dir before nginx starts.
|
||||||
#
|
#
|
||||||
# 🚨 DO NOT "simplify" this into pointing nginx at the CA dir. It does
|
# 🚨 DO NOT "simplify" this into pointing nginx at the CA dir. It does
|
||||||
# TWO jobs: it re-modes the leaf to the 0640 root:nginx copy below (a
|
# TWO jobs: it re-modes the leaf to the 0640 root:nginx copy below (a
|
||||||
|
|
@ -292,18 +297,27 @@ in
|
||||||
# read it — a 0600 root:root key passes the master load but
|
# read it — a 0600 root:root key passes the master load but
|
||||||
# fails the pre-start config test with `BIO_new_file() …
|
# fails the pre-start config test with `BIO_new_file() …
|
||||||
# Permission denied`, blocking the unit. Cert is world-read.
|
# Permission denied`, blocking the unit. Cert is world-read.
|
||||||
install -m 0644 ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/gateway.pem ${tlsCert}
|
${
|
||||||
install -m 0640 -g nginx ${config.services.hyperhive.deploy.hive-controller.tls.stateDir}/gateway-key.pem ${tlsKey}
|
if runsHive then
|
||||||
|
''
|
||||||
|
install -m 0644 ${tlsStateDir}/gateway.pem ${tlsCert}
|
||||||
|
install -m 0640 -g nginx ${tlsStateDir}/gateway-key.pem ${tlsKey}''
|
||||||
|
else
|
||||||
|
''
|
||||||
|
# No hive here, so no hive leaf: the catch-all and any other
|
||||||
|
# vhost on the hive cert path serve the swarm-services leaf.
|
||||||
|
install -m 0644 ${tlsStateDir}/swarm-services.pem ${tlsCert}
|
||||||
|
install -m 0640 -g nginx ${tlsStateDir}/swarm-services-key.pem ${tlsKey}''
|
||||||
|
}
|
||||||
|
|
||||||
# The swarm-services leaf, when this host issues one. It is
|
# The swarm-services leaf, when this host issues one. It is
|
||||||
# a separate pair rather than more SANs on the one above
|
# a separate pair rather than more SANs on the one above
|
||||||
# because no hive CA can sign these names — each is
|
# because the hive leaf's role allows only its own hive's
|
||||||
# constrained to its own hive's domain and the service
|
# domain, and the service names are siblings of it.
|
||||||
# names are siblings of it.
|
|
||||||
#
|
#
|
||||||
# Absent is a normal state, not a failure: the leaf exists
|
# Absent is a normal state, not a failure: a host fronting
|
||||||
# only where the swarm CA is autoconfigured, and issuance
|
# no swarm name has none, and issuance can also fail on a
|
||||||
# can also fail on a host that wants one.
|
# host that wants one.
|
||||||
#
|
#
|
||||||
# ⚠️ When it is absent the HIVE leaf goes to this path
|
# ⚠️ When it is absent the HIVE leaf goes to this path
|
||||||
# anyway, and that fallback is load-bearing rather than
|
# anyway, and that fallback is load-bearing rather than
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ in
|
||||||
|
|
||||||
Which issuer covers a name is **gateway** knowledge, not the
|
Which issuer covers a name is **gateway** knowledge, not the
|
||||||
service's: a swarm service's name can sit outside this hive's
|
service's: a swarm service's name can sit outside this hive's
|
||||||
domain, and the hive CA is name-constrained out of it, so that
|
domain, and the hive leaf's role refuses it, so that
|
||||||
vhost must serve the swarm-services leaf while everything else
|
vhost must serve the swarm-services leaf while everything else
|
||||||
keeps the hive leaf. A service module calls this instead of
|
keeps the hive leaf. A service module calls this instead of
|
||||||
deciding — deciding is how the vhost and the cert stop
|
deciding — deciding is how the vhost and the cert stop
|
||||||
|
|
@ -231,7 +231,7 @@ in
|
||||||
defaultText = lib.literalExpression "tls.certDir == null && !tls.acme.enable";
|
defaultText = lib.literalExpression "tls.certDir == null && !tls.acme.enable";
|
||||||
description = ''
|
description = ''
|
||||||
Read-only derived flag: `true` when the gateway serves the
|
Read-only derived flag: `true` when the gateway serves the
|
||||||
self-signed (hive-CA-signed) leaf — i.e. neither `tls.certDir` nor
|
self-signed leaf the secret store issues — i.e. neither `tls.certDir` nor
|
||||||
`tls.acme.enable` is configured. Single source of truth for the
|
`tls.acme.enable` is configured. Single source of truth for the
|
||||||
self-signed condition; consumed by the `hive-tls` and `hive-ci`
|
self-signed condition; consumed by the `hive-tls` and `hive-ci`
|
||||||
modules so the derivation isn't duplicated. Internal — not meant to
|
modules so the derivation isn't duplicated. Internal — not meant to
|
||||||
|
|
@ -261,7 +261,7 @@ in
|
||||||
Path to a host directory containing a TLS certificate and
|
Path to a host directory containing a TLS certificate and
|
||||||
private key for nginx. When set, nginx listens on `httpsPort`
|
private key for nginx. When set, nginx listens on `httpsPort`
|
||||||
and uses this cert, overriding the self-signed default — the
|
and uses this cert, overriding the self-signed default — the
|
||||||
auto-generated hive-CA-signed leaf is skipped entirely.
|
store-issued self-signed leaf is skipped entirely.
|
||||||
|
|
||||||
nginx reads `<certDir>/<tls.certName>` and
|
nginx reads `<certDir>/<tls.certName>` and
|
||||||
`<certDir>/<tls.keyName>` directly — it runs on the host, so
|
`<certDir>/<tls.keyName>` directly — it runs on the host, so
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@
|
||||||
cfg, # services.hyperhive.gateway
|
cfg, # services.hyperhive.gateway
|
||||||
tlsCert,
|
tlsCert,
|
||||||
tlsKey,
|
tlsKey,
|
||||||
svcCert, # swarm-services leaf, for names the hive CA cannot sign
|
svcCert, # swarm-services leaf, for names the hive leaf cannot carry
|
||||||
svcKey,
|
svcKey,
|
||||||
swarmServiceDomains, # which names those are (../swarm.nix derives it)
|
swarmServiceDomains, # which names those are (../swarm.nix derives it)
|
||||||
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable }
|
errorPages, # ./error-pages.nix: { notFound, unreachable, unauthorized, ssoUnavailable }
|
||||||
caBundle, # hive CA trust bundle on the host (root + intermediate)
|
caBundle, # hive TLS trust bundle on the host
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# nixos `services.nginx.virtualHosts.<name>` ssl attrs for a vhost
|
# nixos `services.nginx.virtualHosts.<name>` ssl attrs for a vhost
|
||||||
|
|
@ -68,8 +68,8 @@ in
|
||||||
];
|
];
|
||||||
|
|
||||||
# TLS attrs for one vhost, by name. A swarm service's name may sit
|
# TLS attrs for one vhost, by name. A swarm service's name may sit
|
||||||
# outside this hive's domain — and then the hive CA is
|
# outside this hive's domain — and then the hive leaf's role
|
||||||
# name-constrained out of it, so its vhost must serve the
|
# refuses it, so its vhost must serve the
|
||||||
# swarm-services leaf instead. Everything else keeps the hive leaf.
|
# swarm-services leaf instead. Everything else keeps the hive leaf.
|
||||||
#
|
#
|
||||||
# Only in self-signed mode: with ACME or an operator cert there is a
|
# Only in self-signed mode: with ACME or an operator cert there is a
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,16 @@ let
|
||||||
# Nor on a host fronting no swarm name: it asks the store for the root
|
# Nor on a host fronting no swarm name: it asks the store for the root
|
||||||
# alone, which nginx never reads, and a sealed store would otherwise hold
|
# alone, which nginx never reads, and a sealed store would otherwise hold
|
||||||
# its gateway down.
|
# its gateway down.
|
||||||
|
#
|
||||||
|
# On a host with no hive there is no hive leaf to start on: the services
|
||||||
|
# leaf is the only certificate its gateway has, so it always waits.
|
||||||
gatewayWaitsForServicesLeaf =
|
gatewayWaitsForServicesLeaf =
|
||||||
localServiceDomains != [ ] && localServiceDomains != [ baoCfg.ui.domain ];
|
localServiceDomains != [ ] && (!runsHive || localServiceDomains != [ baoCfg.ui.domain ]);
|
||||||
|
|
||||||
|
# The hive leaf, `hive-tls-resign` and the hive identity it needs exist only
|
||||||
|
# where a hive runs. A gateway host without one serves the swarm-services
|
||||||
|
# leaf in their place (../hive-gateway/default.nix).
|
||||||
|
runsHive = hyperhiveCfg.deploy.hive-controller.enable;
|
||||||
|
|
||||||
# The gateway's own leaf is only issued when the gateway actually serves a
|
# The gateway's own leaf is only issued when the gateway actually serves a
|
||||||
# self-signed cert: the gateway must run here and be in self-signed mode.
|
# self-signed cert: the gateway must run here and be in self-signed mode.
|
||||||
|
|
@ -458,7 +466,7 @@ in
|
||||||
# propagation failure means the leaf rotates on disk while nginx keeps
|
# propagation failure means the leaf rotates on disk while nginx keeps
|
||||||
# serving the old copy until it expires, with this unit reporting
|
# serving the old copy until it expires, with this unit reporting
|
||||||
# success the whole time.
|
# success the whole time.
|
||||||
systemd.services.hive-tls-resign = {
|
systemd.services.hive-tls-resign = lib.mkIf runsHive {
|
||||||
description = "Issue or renew the gateway TLS leaf from the secret store's PKI";
|
description = "Issue or renew the gateway TLS leaf from the secret store's PKI";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
# The store's container and its granting unit where they run here;
|
# The store's container and its granting unit where they run here;
|
||||||
|
|
@ -941,7 +949,7 @@ in
|
||||||
|
|
||||||
# Daily, for the reason `swarm-services-cert-renew` below is: the leaf
|
# Daily, for the reason `swarm-services-cert-renew` below is: the leaf
|
||||||
# needs the store, which can be sealed or unreachable.
|
# needs the store, which can be sealed or unreachable.
|
||||||
systemd.timers.hive-tls-resign = {
|
systemd.timers.hive-tls-resign = lib.mkIf runsHive {
|
||||||
description = "Daily renewal check for the gateway TLS leaf";
|
description = "Daily renewal check for the gateway TLS leaf";
|
||||||
wantedBy = [ "timers.target" ];
|
wantedBy = [ "timers.target" ];
|
||||||
timerConfig = {
|
timerConfig = {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,14 @@ let
|
||||||
deploy.allSwarmServices = true;
|
deploy.allSwarmServices = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# A services host with hives elsewhere and the store elsewhere too: no hive
|
||||||
|
# and no store identity minted here.
|
||||||
|
swarmServicesOnlyNoStore = hive {
|
||||||
|
deploy.hive-controller.enable = false;
|
||||||
|
deploy.allSwarmServices = true;
|
||||||
|
deploy.bao.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
# The same switch with every service it owns placed elsewhere, so the only
|
# The same switch with every service it owns placed elsewhere, so the only
|
||||||
# thing that can enable the gateway/resolver/bridge here is that toggle's
|
# thing that can enable the gateway/resolver/bridge here is that toggle's
|
||||||
# own module — each service module asserts them from its own `enable`.
|
# own module — each service module asserts them from its own `enable`.
|
||||||
|
|
@ -187,6 +195,36 @@ let
|
||||||
&& swarmServicesSwitchAlone.services.hyperhive.gateway.dns.enable
|
&& swarmServicesSwitchAlone.services.hyperhive.gateway.dns.enable
|
||||||
&& swarmServicesSwitchAlone.services.hyperhive.network.enable;
|
&& swarmServicesSwitchAlone.services.hyperhive.network.enable;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
# No hive, so no hive leaf and no hive identity: the gateway's default
|
||||||
|
# certificate is the swarm-services leaf, and its start waits for that
|
||||||
|
# leaf. The control is a hive host with the same services, which keeps
|
||||||
|
# the hive leaf and the unit that issues it.
|
||||||
|
name = "a gateway host without a hive serves the swarm-services leaf as its default cert and has no hive-leaf unit";
|
||||||
|
ok =
|
||||||
|
let
|
||||||
|
noHive =
|
||||||
|
m:
|
||||||
|
let
|
||||||
|
s = m.systemd.services;
|
||||||
|
imp = s.hive-gateway-self-signed-cert.script;
|
||||||
|
in
|
||||||
|
!(s ? hive-tls-resign)
|
||||||
|
&& !(m.systemd.timers ? hive-tls-resign)
|
||||||
|
&& lib.hasInfix "install -m 0644 /var/lib/hive-tls/swarm-services.pem /var/lib/hive-gateway/tls/cert.pem" imp
|
||||||
|
&& !(lib.hasInfix "/var/lib/hive-tls/gateway.pem /var/lib/hive-gateway/tls/cert.pem" imp)
|
||||||
|
&& lib.elem "hive-gateway-self-signed-cert.service" s.swarm-services-cert.requiredBy
|
||||||
|
&& lib.elem "hive-gateway-self-signed-cert.service" s.swarm-services-cert.before
|
||||||
|
# Fronting swarm names, so the root-only branch that presents the
|
||||||
|
# hive identity is not the one that runs.
|
||||||
|
&& lib.hasInfix "want_svc=1" s.swarm-services-cert.script;
|
||||||
|
hs = swarmServicesHere.systemd.services;
|
||||||
|
in
|
||||||
|
noHive swarmServicesOnly
|
||||||
|
&& noHive swarmServicesOnlyNoStore
|
||||||
|
&& hs ? hive-tls-resign
|
||||||
|
&& lib.hasInfix "install -m 0644 /var/lib/hive-tls/gateway.pem /var/lib/hive-gateway/tls/cert.pem" hs.hive-gateway-self-signed-cert.script;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
# Each service is gated on its own deployment toggle alone, so a host
|
# Each service is gated on its own deployment toggle alone, so a host
|
||||||
# that runs no hive still runs every shared service the switch turns
|
# that runs no hive still runs every shared service the switch turns
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue