A machine's filtering is computed from what it was assigned
A rule nobody derives is a rule somebody keeps in step by hand, and five HAL manifests carry a `scope:` key that reads as a restriction and restricts nothing. Both halves are closed here. Manifests are parsed strictly. An unknown key is refused, which is the discipline the host's declaration parser has always had; `scope:` survived because nothing rejected it. A module says what it listens on and who may reach it, and saying from where is required — a rule with no source is open, and must say so rather than appear to restrict something. The mesh gathers every assigned module's ports, widens where two overlap, names every module that wanted each one, and renders one nftables file per node. What no module declared is closed. Three things it deliberately does not do: it writes no forward policy, because what a machine routes is the container runtime's business and dropping there stops every container on the node; it never flushes the whole ruleset, only its own table; and it carries no command to load itself, because the link may not carry an action. A service declares `restart-on` the file instead, which is the shape that rule leaves. Also fixes a fault the lab found: certificateFor asked where every node is without the catalogue, so nothing resolved, every machine looked like it was on no private network, and every certificate the mesh was asked for was refused with a reason that was not true. Asking that question without the catalogue is now refused rather than answered wrongly.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
@@ -212,6 +213,24 @@ type Manifest struct {
|
||||
// module, in a file anybody can read, for ever.
|
||||
Needs map[string]string `json:"needs,omitempty"`
|
||||
|
||||
// Listens is what this module accepts connections on, and from where.
|
||||
//
|
||||
// **A rule names its source** ([ADR 0007](novox/hq)). A port with no source is open to
|
||||
// everything that can reach the machine, and saying so is the difference between a manifest
|
||||
// that restricts something and one that appears to — which is the fault
|
||||
// [04-ISSUES/003](novox/hq) records, where five manifests carried a `scope:` nothing read.
|
||||
//
|
||||
// **Derived, not kept in step by hand.** A machine's open ports are a consequence of what runs
|
||||
// on it; the mesh gathers these and hands the whole set to whatever enforces them.
|
||||
Listens []Listening `json:"listens,omitempty"`
|
||||
|
||||
// Filtering is where this module wants the node's whole computed rule set written.
|
||||
//
|
||||
// One module per node asks for it, and what it receives is derived from every module's
|
||||
// `listens` rather than from its own — a firewall is a property of the machine, and a module
|
||||
// that could only see its own ports would write a rule set that closed everything else.
|
||||
Filtering *Filtering `json:"filtering,omitempty"`
|
||||
|
||||
// Certificate is where this module wants a certificate for its machine's name inside the
|
||||
// mesh, and where the key that goes with it can be found.
|
||||
//
|
||||
@@ -274,6 +293,46 @@ const (
|
||||
ArtifactUpstream = "upstream"
|
||||
)
|
||||
|
||||
// Listening is one port a module accepts connections on.
|
||||
type Listening struct {
|
||||
Port int `json:"port"`
|
||||
// Protocol is "tcp" or "udp". Absent means tcp, which is what almost everything is — and a
|
||||
// field that had to be written every time would be written wrongly some of the time.
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
// From is who may reach it. Required, because a rule with no source is open and must say so
|
||||
// rather than appear to restrict something.
|
||||
From string `json:"from"`
|
||||
// Why this port is open, for somebody reading a generated rule set and wondering.
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// Where a listening port may be reached from.
|
||||
const (
|
||||
// FromMesh is any machine on the private network. What almost everything wants.
|
||||
FromMesh = "mesh"
|
||||
// FromEverywhere is the public internet. Deliberately spelled out: a port open to everything
|
||||
// should be legible as such in the manifest, not the consequence of an omission.
|
||||
FromEverywhere = "anywhere"
|
||||
// FromMachine is this machine only — a port bound for something else on the same host.
|
||||
FromMachine = "machine"
|
||||
)
|
||||
|
||||
// At is this port's protocol, with the default applied.
|
||||
func (l Listening) At() string {
|
||||
if l.Protocol == "" {
|
||||
return "tcp"
|
||||
}
|
||||
return l.Protocol
|
||||
}
|
||||
|
||||
// Filtering says where a module wants the computed rule set.
|
||||
type Filtering struct {
|
||||
// Into is the path to write it to. Whatever loads it is this module's own business — an
|
||||
// action beside this field, ordinarily — because how a machine enforces rules is a fact about
|
||||
// the machine and the mesh has no business knowing it.
|
||||
Into string `json:"into"`
|
||||
}
|
||||
|
||||
// Certificate says where a module wants what the mesh issued for its machine.
|
||||
type Certificate struct {
|
||||
// Into is where the certificate is written.
|
||||
@@ -287,6 +346,10 @@ type Certificate struct {
|
||||
func CertificateID() string { return "certificate" }
|
||||
func AuthorityID() string { return "certificate-authority" }
|
||||
|
||||
// FilteringID names the computed rule set, so it is the same resource across every declaration
|
||||
// and a change to it is an update rather than an addition beside the old one.
|
||||
func FilteringID() string { return "filtering" }
|
||||
|
||||
// NeedID is the resource identity of the file a module's own secret lands in.
|
||||
func NeedID(name string) string { return "needs-" + name }
|
||||
|
||||
@@ -331,7 +394,16 @@ func ReceivedID(requirement string) string { return "received-" + requirement }
|
||||
// them in one pass or in four.
|
||||
func ParseManifest(raw []byte) (Manifest, error) {
|
||||
var m Manifest
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
// Strictly. **An unknown key is refused**, which is the discipline the host's declaration
|
||||
// parser has and manifests lacked (novox/hq 04-ISSUES/003): a `scope:` key survived in five
|
||||
// manifests, read by nothing, making them appear to restrict a port and restrict nothing.
|
||||
//
|
||||
// "An unenforced rule is indistinguishable from a wrong one, and costs more, because people
|
||||
// believe it" — and a silently-accepted key is worse than unenforced, because a reviewer
|
||||
// checking whether something is restricted will find that it is, and be wrong.
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&m); err != nil {
|
||||
return Manifest{}, fmt.Errorf("this is not a module manifest: %w", err)
|
||||
}
|
||||
|
||||
@@ -429,6 +501,33 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
"%s binds %q and does not require it", m.Module, to))
|
||||
}
|
||||
}
|
||||
if f := m.Filtering; f != nil && strings.TrimSpace(f.Into) == "" {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s asks for the computed rule set and does not say where to put it", m.Module))
|
||||
}
|
||||
for _, l := range m.Listens {
|
||||
if l.Port < 1 || l.Port > 65535 {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s listens on port %d, which is not a port", m.Module, l.Port))
|
||||
}
|
||||
switch l.From {
|
||||
case FromMesh, FromEverywhere, FromMachine:
|
||||
case "":
|
||||
// The fault this field exists to prevent. A rule with no source is open, and a
|
||||
// manifest that omitted it would read as a restriction and be none.
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s listens on %d and does not say from where; it is %q, %q or %q",
|
||||
m.Module, l.Port, FromMesh, FromEverywhere, FromMachine))
|
||||
default:
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s listens on %d from %q; it is %q, %q or %q",
|
||||
m.Module, l.Port, l.From, FromMesh, FromEverywhere, FromMachine))
|
||||
}
|
||||
if p := l.At(); p != "tcp" && p != "udp" {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s listens on %d over %q, which is tcp or udp", m.Module, l.Port, p))
|
||||
}
|
||||
}
|
||||
if c := m.Certificate; c != nil {
|
||||
if !strings.HasPrefix(c.Into, "/") {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
|
||||
Reference in New Issue
Block a user