swarm-nats: give the queue a name, a bao-issued leaf, and require TLS
The queue listened in plaintext on 4222, reached by bridge IP or loopback,
and nothing in-tree opened it to another hive. It now has a name, serves a
certificate for that name alone, and refuses clients that do not speak TLS.
- `swarm.nats.domain`, default `nats.<swarm.domain>`, a sibling name like
`swarm.bao.domain`. The queue host answers it via `gateway.localNames`;
every other hive resolves it through the operator's DNS, as for bao.
- `pki/roles/swarm-nats` allows that one name (bare domain, no subdomains,
IPs or localhost, server flag). A `swarm-nats` cert-auth role and policy
may only `update` `pki/issue/swarm-nats`, written by
`swarm-bao-nats-tls-policy`. The login leaf is minted by glue-bao-tls and
paired by glue-nats-bao-identity. `deploy.bao.natsCommonName` is reserved
as a hive name.
- `swarm-bao-nats-tls` issues the leaf into a directory bound read-only into
the container, restarts nats when it rotates, and re-runs daily.
It joins glue-bao-readers-policy-order, so it is ordered after its policy
unit (`after` and `wants`, never `requires`) where the store is on the
same host. The policy unit joins the store's journald list.
- nats gets `tls {}`, with the key via `LoadCredential`, and no
`allow_non_tls`. `validateConfig` is now off in every mode, because the
build-time check loads a leaf that only exists at runtime.
- 4222 is also open on `wg-hive` when the host is on the mesh, never
host-wide.
- `statusPublish.natsUrl`, `queue.agentNatsUrl`, the controller's URL under
`singleHostSwarm`, and the auth responder all dial
`tls://<swarm.nats.domain>:<port>`. swarm-queue-client hands its CA file
to the NATS connection too, so hive-c0re and the controller trust the
leaf's root.
- docs/swarm/README.md: the queue URL and the one DNS record a multi-host
swarm needs.
module-eval-nats-tls pins the role, the policy, the served leaf, the
firewall, the ordering, and a scan of every `*_NATS_URL` and the
responder's URL across the host and its containers.
Closes #4626
This commit is contained in:
parent
244db367c4
commit
1d261b3fed
17 changed files with 748 additions and 108 deletions
|
|
@ -384,6 +384,17 @@ let
|
|||
}
|
||||
'';
|
||||
|
||||
# The queue's principal: one `update` on its own role, the shape of the
|
||||
# services issuer's grant above, narrowed to one name by the role itself.
|
||||
natsPolicyName = "swarm-nats";
|
||||
natsCn = baoDeploy.natsCommonName;
|
||||
natsPkiRoleName = baoDeploy.natsPkiRoleName;
|
||||
natsPolicyText = ''
|
||||
path "${servicesPkiMountPath}/issue/${natsPkiRoleName}" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
'';
|
||||
|
||||
# ── the four principals that used to share the hive's own leaf ─────────────
|
||||
#
|
||||
# 🩸 Each of the four reads exactly ONE path in the store, and until this
|
||||
|
|
@ -1074,6 +1085,35 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
natsCommonName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "swarm-nats";
|
||||
description = ''
|
||||
Subject the store's `swarm-nats` cert-auth role accepts: the identity
|
||||
the queue's host presents to ask for the queue's TLS leaf.
|
||||
|
||||
Its own principal rather than
|
||||
{option}`services.hyperhive.deploy.bao.servicesIssuerCommonName`,
|
||||
whose role issues for every name in
|
||||
{option}`services.hyperhive.swarm.serviceDomains`. This one may issue
|
||||
from {option}`services.hyperhive.deploy.bao.natsPkiRoleName` alone,
|
||||
and that role only for {option}`services.hyperhive.swarm.nats.domain`.
|
||||
|
||||
⚠️ Reserved as a hive name by ./swarm.nix, like its siblings.
|
||||
'';
|
||||
};
|
||||
|
||||
natsPkiRoleName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "swarm-nats";
|
||||
description = ''
|
||||
Role on {option}`services.hyperhive.deploy.bao.servicesPkiMountPath`
|
||||
the queue's TLS leaf is issued through. It allows exactly
|
||||
{option}`services.hyperhive.swarm.nats.domain`: no subdomains, IPs or
|
||||
localhost.
|
||||
'';
|
||||
};
|
||||
|
||||
matrixCtlHiveName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = toString hyperhiveCfg.hiveName;
|
||||
|
|
@ -1518,6 +1558,7 @@ in
|
|||
"swarm-bao-grafana-oidc-policy"
|
||||
"swarm-bao-otel-oidc-policy"
|
||||
"swarm-bao-services-issuer-policy"
|
||||
"swarm-bao-nats-tls-policy"
|
||||
];
|
||||
|
||||
# 🚫 No `swarm.otel.scrapeTargets.bao` entry any more, and its absence is
|
||||
|
|
@ -2216,6 +2257,70 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
# A FIFTH sibling: the queue's issuing role, its policy and its login
|
||||
# role, together. The `pki` role lives here rather than beside
|
||||
# `swarm-services` in the controller's unit because it belongs to this
|
||||
# principal; that unit creates the mount, hence the `after`.
|
||||
#
|
||||
# The role narrows exactly as `swarm-services` does, to one name. It is
|
||||
# the queue's domain alone, since the same name reaches it from every
|
||||
# hive; no IP SANs, since nothing dials an address.
|
||||
systemd.services.swarm-bao-nats-tls-policy = lib.mkIf haveBootstrapToken {
|
||||
description = "write the swarm queue's pki role, bao policy and cert-auth role";
|
||||
after = [
|
||||
"container@${cfg.machine}.service"
|
||||
"swarm-bao-controller-policy.service"
|
||||
];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [
|
||||
baoCli
|
||||
pkgs.coreutils
|
||||
];
|
||||
unitConfig.ConditionPathExists = baoDeploy.bootstrapTokenFile;
|
||||
# Same unseal wait as its siblings above.
|
||||
startLimitBurst = 2880;
|
||||
startLimitIntervalSec = 90000;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
Restart = "on-failure";
|
||||
RestartSec = 30;
|
||||
};
|
||||
script = ''
|
||||
set -euo pipefail
|
||||
|
||||
BAO_TOKEN="$(cat ${lib.escapeShellArg baoDeploy.bootstrapTokenFile})"
|
||||
export BAO_TOKEN
|
||||
|
||||
bao write ${lib.escapeShellArg "${servicesPkiMountPath}/roles/${natsPkiRoleName}"} \
|
||||
allowed_domains=${lib.escapeShellArg hyperhiveCfg.swarm.nats.domain} \
|
||||
allow_bare_domains=true \
|
||||
allow_subdomains=false \
|
||||
allow_glob_domains=false \
|
||||
allow_localhost=false \
|
||||
allow_any_name=false \
|
||||
allow_ip_sans=false \
|
||||
enforce_hostnames=true \
|
||||
server_flag=true \
|
||||
client_flag=false \
|
||||
key_type=rsa \
|
||||
key_bits=4096 \
|
||||
ttl=${servicesPkiLeafTtl} \
|
||||
max_ttl=${servicesPkiLeafTtl}
|
||||
|
||||
printf '%s' ${lib.escapeShellArg natsPolicyText} |
|
||||
bao policy write ${lib.escapeShellArg natsPolicyName} -
|
||||
''
|
||||
+ lib.optionalString (baoDeploy.clientCaFile != null) ''
|
||||
|
||||
bao write auth/cert/certs/${lib.escapeShellArg natsPolicyName} \
|
||||
certificate=@${tlsDir}/client-ca.pem \
|
||||
allowed_common_names=${lib.escapeShellArg natsCn} \
|
||||
token_policies=${lib.escapeShellArg natsPolicyName} \
|
||||
display_name=${lib.escapeShellArg natsCn}
|
||||
'';
|
||||
};
|
||||
|
||||
# The CA bind source is written at runtime by a host unit, so the
|
||||
# container has to start after it — otherwise nspawn sets up a mount
|
||||
# over a file that does not exist yet.
|
||||
|
|
|
|||
Loading…
Reference in a new issue