skip to content

A Web Platform

A small web company runs six hosts. Each host has an environment (dev or prod), a datacenter (us-east or us-west) and a role (webserver or database). One rule decides the access list: a webserver may reach a database in the same environment, unless that database is quarantined.

hostenvironmentdatacenterrole
web-dev-eastdevus-eastwebserver
web-prod-eastprodus-eastwebserver
web-prod-westprodus-westwebserver
db-dev-eastdevus-eastdatabase
db-prod-eastprodus-eastdatabase
db-prod-westprodus-westdatabase, quarantined

The access list is a set of edges between hosts. gen-program solves the rule, and gen-graph queries the edges it produces.

let
gen = inputs.gen.lib.mkGenLibs { };
inherit (gen) program graph;
hosts = {
web-dev-east = { env = "dev"; dc = "us-east"; role = "webserver"; };
web-prod-east = { env = "prod"; dc = "us-east"; role = "webserver"; };
web-prod-west = { env = "prod"; dc = "us-west"; role = "webserver"; };
db-dev-east = { env = "dev"; dc = "us-east"; role = "database"; };
db-prod-east = { env = "prod"; dc = "us-east"; role = "database"; };
db-prod-west = { env = "prod"; dc = "us-west"; role = "database"; quarantined = true; };
};
names = builtins.attrNames hosts;
withRole = role: builtins.filter (h: hosts.${h}.role == role) names;
fact = head: relata: { inherit head relata; };
# (1) facts read off the host data
facts =
map (h: fact "role:${hosts.${h}.role}:${h}" [ h ]) names
++ builtins.concatMap (w:
builtins.concatMap (d:
if hosts.${w}.env == hosts.${d}.env then [ (fact "sameEnv:${w}:${d}" [ w d ]) ] else [ ]
) (withRole "database")
) (withRole "webserver")
++ map (h: fact "quarantined:${h}" [ h ])
(builtins.filter (h: hosts.${h}.quarantined or false) names);
# (2) the rule
rules = builtins.concatMap (w:
map (d: {
head = "reaches:${w}:${d}";
label = "reaches";
pos = [ "role:webserver:${w}" "role:database:${d}" "sameEnv:${w}:${d}" ];
neg = [ "quarantined:${d}" ];
relata = [ w d ];
}) (withRole "database")
) (withRole "webserver");
# (3) solve
declarations = facts ++ rules;
model = program.model {
prior = null;
program = program.program names declarations;
interpretation = [ ];
complete = true;
};
# (4) the edge of every labelled rule whose head the model includes
acl = (program.ruleEdges model declarations).reached;
# (5) query the result as a graph
g = {
edges = id: map (e: e.to) (builtins.filter (e: e.from == id) acl);
parent = _: null;
nodes = names;
nodeData = id: hosts.${id};
};
in
{
inherit acl;
reachableFromWebProdWest = graph.reachableFrom g "web-prod-west";
whoReachesDbProdEast = graph.dependents { } g "db-prod-east";
usEast = graph.select g (d: d.dc == "us-east");
quarantined = (model.resolve "reaches:web-prod-east:db-prod-west").included;
}
  1. Facts are rules with no conditions. They state each host’s role, which webserver and database pairs share an environment, and which hosts are quarantined.
  2. One rule per webserver and database pair. pos lists the atoms that must be true and neg the atoms that must be false.
  3. model solves the program. Every atom is true, false or undefined.
  4. A rule with a label denotes an edge from its first relatum to its second. ruleEdges returns those whose head the model includes, and fails with an error on an undefined head rather than dropping it.
  5. The edges form an ordinary accessor graph, so every gen-graph query works on them.

It evaluates to:

{
acl = [
{ from = "web-dev-east"; label = "reaches"; to = "db-dev-east"; }
{ from = "web-prod-east"; label = "reaches"; to = "db-prod-east"; }
{ from = "web-prod-west"; label = "reaches"; to = "db-prod-east"; }
];
reachableFromWebProdWest = [ "db-prod-east" ];
whoReachesDbProdEast = [ "web-prod-east" "web-prod-west" ];
usEast = [ "db-dev-east" "db-prod-east" "web-dev-east" "web-prod-east" ];
quarantined = false;
}

No dev webserver reaches a prod database. db-prod-west is in prod, but its quarantine fact makes the negative condition fail, so no webserver reaches it. web-prod-west crosses datacenters to reach db-prod-east. To keep traffic inside a datacenter, add a sameDc fact and put it in the rule’s pos list.

part of the examplewhat it isread more
hosts with environment, datacenter and rolenodes and their attributesYour Configuration as a Graph
facts, rules, program.modela stratified logic program and its well-founded modelRules That Add Relationships
pos, neg, model.resolvepositive and negative conditions and each atom’s verdictRules
acl, g, reachableFrom, dependents, selectedges and queries over themGraphs and Queries
asking why an edge existsgen-inspect over the program and its modelInspection

In a full configuration the hosts are instances in a registry, as in A Small Deployment, and the firewall rules on each database host are built from whoReaches… queries like the one above.

palette
dark
light
↑↓ select↵ apply esc close

Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).