mesh/merge-gate pass: builds new: modules/mesh-reads, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
Reading a file, searching one, listing a directory, the owner and mode of each component of a path and looking up an account went through the login shell's execute as the operator's account, which sees the operator's keys and the agent's credentials. This module holds node-reads on every node and runs as mesh-reads (no login, no group beside its own). It refuses the node's secret places by name, judging the file actually opened rather than the name asked, so a flipped link reaches nothing refused; withholds any line shaped like a secret through mesh-sdk's outward rule and every line of a BEGIN/END block; and bounds every answer. Replaces mesh-catalog #244 (reviews 55), whose first commit carried a built binary.
130 lines
5.7 KiB
Go
130 lines
5.7 KiB
Go
package main
|
||
|
||
import (
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"git.novox.be/novox/mesh-sdk/go/outward"
|
||
)
|
||
|
||
// What is refused by name, before any read (novox/hq ADR 0310 §5), and what an answer withholds (§4).
|
||
//
|
||
// **Two lists of places.** The places the mesh knows a secret is kept on this node — every module's own-secret
|
||
// directory, every credential file and grant directory, the vault's directories — come from the controller,
|
||
// which composes them from the manifests (MESH_SECRET_PLACES). The known credential files of the programs
|
||
// on a machine are this module's list, below: the mesh does not write them, so nothing else names them. Both
|
||
// hold whatever the account's file modes would allow: the file modes are the first bound, the names the second.
|
||
|
||
// knownDirs are directories that hold nothing but credentials, wherever they are: an account's keys.
|
||
var knownDirs = map[string]bool{
|
||
".ssh": true, ".gnupg": true, ".password-store": true, ".aws": true, ".kube": true, ".azure": true,
|
||
".mozilla": true, ".thunderbird": true, // browser and mail profiles: saved logins and cookies
|
||
}
|
||
|
||
// knownNames are file names that are a credential whatever directory they are in.
|
||
var knownNames = map[string]bool{
|
||
".credentials.json": true, // the agent's credentials (Claude Code)
|
||
".netrc": true, ".pgpass": true, ".git-credentials": true, ".npmrc": true, ".pypirc": true,
|
||
".my.cnf": true, ".vault-token": true, "credentials": true, "credentials.json": true,
|
||
"id_rsa": true, "id_dsa": true, "id_ecdsa": true, "id_ed25519": true, "id_ecdsa_sk": true, "id_ed25519_sk": true,
|
||
"shadow": true, "gshadow": true, "shadow-": true, "gshadow-": true, "opasswd": true,
|
||
"broker": true, // the mesh's name for a module's bus credential
|
||
// Shell and program histories: where a token is typed.
|
||
".bash_history": true, ".zsh_history": true, ".histfile": true, ".python_history": true, ".psql_history": true,
|
||
".mysql_history": true, ".lesshst": true, ".viminfo": true, ".node_repl_history": true, ".sqlite_history": true,
|
||
".rediscli_history": true,
|
||
}
|
||
|
||
// knownSuffixes are file name endings a credential has.
|
||
var knownSuffixes = []string{".key", ".p12", ".pfx", ".jks", ".keystore", ".creds", ".nk", ".kdbx", "_key"}
|
||
|
||
// knownPaths are the places on a machine that hold credentials and are not a module's.
|
||
var knownPaths = []string{
|
||
"/etc/ssl/private", "/etc/wireguard", "/etc/NetworkManager/system-connections", "/etc/ipsec.d/private",
|
||
"/var/lib/iwd", "/etc/docker/key.json", "/root/.docker/config.json", "/var/lib/sss/secrets",
|
||
"/etc/rancher", "/etc/kubernetes/admin.conf",
|
||
}
|
||
|
||
// knownProfiles are directories under an account's .config that hold a browser's saved logins and cookies.
|
||
var knownProfiles = map[string]bool{"google-chrome": true, "chromium": true, "BraveSoftware": true, "vivaldi": true,
|
||
"microsoft-edge": true}
|
||
|
||
// procPrivate are the entries of a process under /proc that reach its memory, its environment or its files.
|
||
var procPrivate = map[string]bool{"environ": true, "mem": true, "fd": true, "map_files": true, "root": true,
|
||
"cwd": true}
|
||
|
||
// Refuser says whether a path is a place a secret is kept.
|
||
type Refuser struct {
|
||
// Places are the mesh's, from the controller: refused with everything beneath them.
|
||
Places []string
|
||
}
|
||
|
||
// Refused says why a path may not be read, or "" when it may. The path is clean and absolute.
|
||
func (r Refuser) Refused(path string) string {
|
||
path = filepath.Clean(path)
|
||
for _, p := range r.Places {
|
||
if within(path, p) {
|
||
return "it is a place the mesh keeps a secret (" + p + ")"
|
||
}
|
||
}
|
||
for _, p := range knownPaths {
|
||
if within(path, p) {
|
||
return "it is a place a machine keeps credentials (" + p + ")"
|
||
}
|
||
}
|
||
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
|
||
if len(parts) >= 3 && parts[0] == "proc" && procPrivate[parts[2]] {
|
||
return "it reaches a process's " + parts[2]
|
||
}
|
||
for i, part := range parts {
|
||
if i > 0 && parts[i-1] == ".config" && knownProfiles[part] {
|
||
return "it is " + part + "'s profile, which keeps saved logins"
|
||
}
|
||
if knownDirs[part] {
|
||
return "it is in " + part + ", which holds an account's credentials"
|
||
}
|
||
if i == len(parts)-1 {
|
||
if knownNames[part] {
|
||
return part + " is a known credential file"
|
||
}
|
||
for _, s := range knownSuffixes {
|
||
if strings.HasSuffix(part, s) && len(part) > len(s) {
|
||
return part + " is named as a key or a credential (" + s + ")"
|
||
}
|
||
}
|
||
if strings.HasSuffix(part, ".pem") && (strings.Contains(part, "key") || strings.Contains(part, "private")) {
|
||
return part + " is named as a private key"
|
||
}
|
||
if part == "config.json" && i > 0 && parts[i-1] == ".docker" {
|
||
return "a container registry login is kept there"
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func within(path, place string) bool {
|
||
return path == place || strings.HasPrefix(path, strings.TrimSuffix(place, "/")+"/")
|
||
}
|
||
|
||
// Withheld is one piece of an answer the content rule kept back: where it was, and the class it carried.
|
||
type Withheld struct {
|
||
At string `json:"at"`
|
||
Class string `json:"why"`
|
||
}
|
||
|
||
// clean is the content rule's secret class (mesh-sdk outward, novox/hq ADR 0310 §4) over one piece of an
|
||
// answer: the piece, or a mark in its place when it is shaped like a secret.
|
||
//
|
||
// **What a file holds, not what the file system names.** It judges a file's lines, a matching line, a link's
|
||
// target, a mount's source and an account's words. A path and an entry's name are not judged: they are what
|
||
// the caller named or the file system calls a thing, and the rule's random-run test takes a temporary
|
||
// directory's or a container layer's name for a token, which would withhold the very survey asked for. A
|
||
// place where a secret sits by name is refused before any read instead (Refuser).
|
||
func clean(s string) (string, string) {
|
||
if refusal, ok := outward.CheckSecret(s); !ok {
|
||
return "‹withheld: " + refusal.What + "›", refusal.What
|
||
}
|
||
return s, ""
|
||
}
|