bastion 0.58.1
This commit is contained in:
+2
-2
@@ -624,8 +624,8 @@ func checkPairConflict(cfgs []*Config, handle string, replace bool) (*Config, er
|
||||
}
|
||||
|
||||
// nextGitProxyAddr picks the first loopback port from 7978 up that no existing
|
||||
// pairing claims: the per-host git proxy (git.privatae.ai is reachable only through
|
||||
// the bastion → host → platform path). Allocated lazily for pairings that predate it.
|
||||
// pairing claims: the per-host git proxy, through which local git reaches
|
||||
// git.privatae.ai via that host (gitlane.go). Allocated lazily for pairings that predate it.
|
||||
func nextGitProxyAddr(cfgs []*Config) string {
|
||||
used := map[string]bool{}
|
||||
for _, c := range cfgs {
|
||||
|
||||
+83
-10
@@ -276,27 +276,82 @@ func dcnResolvePath(root, rpath string) (string, bool) {
|
||||
return joined, true
|
||||
}
|
||||
|
||||
// dcnExcluded reports whether rpath trips any exclude pattern. Patterns match the
|
||||
// basename and every path segment (glob per segment: ".ssh", "*.key", ".env*"),
|
||||
// compared the way this platform's file system compares names (dcnNameKey).
|
||||
// dcnExcluded reports whether rpath trips any exclude pattern, or is inside a
|
||||
// local search agent's folder (dcnInAgentDirOn), which no grant reaches. A
|
||||
// pattern of one folder name matches any segment of the path (".ssh", "*.key",
|
||||
// ".env*"); a pattern with a "/" (".config/gcloud") matches that run of
|
||||
// consecutive segments wherever it sits in the path, each one a glob. Names
|
||||
// are compared the way this platform's file system compares them (dcnNameKey).
|
||||
// rpath is a real relative path; a path a host sent goes through dcnExcludedAsSent.
|
||||
func dcnExcluded(excludes []string, rpath string) bool {
|
||||
return dcnExcludedOn(runtime.GOOS, excludes, rpath)
|
||||
}
|
||||
|
||||
func dcnExcludedOn(goos string, excludes []string, rpath string) bool {
|
||||
if dcnInAgentDirOn(goos, rpath) {
|
||||
return true
|
||||
}
|
||||
segs := strings.Split(filepath.ToSlash(rpath), "/")
|
||||
for i, seg := range segs {
|
||||
segs[i] = dcnNameKey(goos, seg)
|
||||
}
|
||||
for _, pat := range excludes {
|
||||
pat = strings.TrimSpace(pat)
|
||||
if pat == "" {
|
||||
continue
|
||||
}
|
||||
pat = dcnNameKey(goos, pat)
|
||||
for _, seg := range segs {
|
||||
if ok, _ := filepath.Match(pat, dcnNameKey(goos, seg)); ok {
|
||||
return true
|
||||
var run []string
|
||||
for _, p := range strings.Split(pat, "/") {
|
||||
if p != "" {
|
||||
run = append(run, dcnNameKey(goos, p))
|
||||
}
|
||||
}
|
||||
if dcnRunMatches(run, segs) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// dcnRunMatches says whether the globs in run match len(run) consecutive
|
||||
// segments of segs, starting anywhere. An empty run matches nothing.
|
||||
func dcnRunMatches(run, segs []string) bool {
|
||||
if len(run) == 0 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i+len(run) <= len(segs); i++ {
|
||||
all := true
|
||||
for j, p := range run {
|
||||
if ok, _ := filepath.Match(p, segs[i+j]); !ok {
|
||||
all = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if all {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// dcnAgentDir is the folder the local search agent keeps everything it makes
|
||||
// in, beside the files it reads: the plaintext of every file in scope, the
|
||||
// sanitized text, vectors, the catalog and sealed bundles. The bastion puts
|
||||
// it at <root>/.agent wherever it may write into the folder (freeagent_managed.go;
|
||||
// the agent's own default too), and a folder inside a store can hold another
|
||||
// store's. The agent's walker never reads one, at any depth and in any case;
|
||||
// the file ops never serve one either.
|
||||
const dcnAgentDir = ".agent"
|
||||
|
||||
// dcnInAgentDirOn reports whether any segment of rpath names an agent's
|
||||
// folder, as the file system on goos names it, and in any case on every OS
|
||||
// (as the agent's own walker judges it: a ".Agent" on a volume that ignores
|
||||
// case is the agent's folder).
|
||||
func dcnInAgentDirOn(goos, rpath string) bool {
|
||||
for _, seg := range strings.Split(filepath.ToSlash(rpath), "/") {
|
||||
if strings.EqualFold(dcnNameKey(goos, seg), dcnAgentDir) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -328,11 +383,25 @@ func dcnNameKey(goos, name string) string {
|
||||
//
|
||||
// A path cleanRel refuses is judged as sent (the sandbox refuses it anyway).
|
||||
func dcnExcludedAsSent(excludes []string, root, rpath string) bool {
|
||||
return dcnJudgedAsSent(root, rpath, func(rel string) bool { return dcnExcluded(excludes, rel) })
|
||||
}
|
||||
|
||||
// dcnAgentAsSent is whether a path a host sent opens an agent's folder (by its
|
||||
// spelling or through a link), judged as dcnExcludedAsSent judges excludes.
|
||||
// dcnExcluded refuses the same paths; this one exists so the refusal can say
|
||||
// why.
|
||||
func dcnAgentAsSent(root, rpath string) bool {
|
||||
return dcnJudgedAsSent(root, rpath, func(rel string) bool { return dcnInAgentDirOn(runtime.GOOS, rel) })
|
||||
}
|
||||
|
||||
// dcnJudgedAsSent applies judge to what a host's path opens: the cleaned path
|
||||
// relative to the root, then where it leads relative to the resolved root.
|
||||
func dcnJudgedAsSent(root, rpath string, judge func(rel string) bool) bool {
|
||||
joined, ok := cleanRel(root, rpath)
|
||||
if !ok {
|
||||
return dcnExcluded(excludes, rpath)
|
||||
return judge(rpath)
|
||||
}
|
||||
if rel, err := filepath.Rel(filepath.Clean(root), joined); err != nil || dcnExcluded(excludes, rel) {
|
||||
if rel, err := filepath.Rel(filepath.Clean(root), joined); err != nil || judge(rel) {
|
||||
return true
|
||||
}
|
||||
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
||||
@@ -343,7 +412,7 @@ func dcnExcludedAsSent(excludes []string, root, rpath string) bool {
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return dcnExcluded(excludes, rel)
|
||||
return judge(rel)
|
||||
}
|
||||
|
||||
// dcnWhereItLeads is p with its deepest existing part's links resolved and the
|
||||
@@ -428,6 +497,10 @@ func dcnLoadGrantMask(ws *websocket.Conn, dir, op string, storeID int64, rpath,
|
||||
dcnRefuse(ws, dir, op, storeID, rpath, "op '"+mask+"' not in the store's granted mask")
|
||||
return nil
|
||||
}
|
||||
if rpath != "" && dcnAgentAsSent(terms.Root, rpath) {
|
||||
dcnRefuse(ws, dir, op, storeID, rpath, "path is in the local search agent's own folder (.agent), which no grant reaches")
|
||||
return nil
|
||||
}
|
||||
if rpath != "" && dcnExcludedAsSent(terms.Excludes, terms.Root, rpath) {
|
||||
dcnRefuse(ws, dir, op, storeID, rpath, "path is excluded by the grant")
|
||||
return nil
|
||||
|
||||
@@ -142,6 +142,10 @@ func dcnCheckPlan(root string, excludes []string, moves []dcnMove) []dcnPlanProb
|
||||
continue
|
||||
}
|
||||
seenTo[pathKey(dst)], seenFrom[pathKey(src)] = i, i
|
||||
if dcnAgentAsSent(root, m.From) || dcnAgentAsSent(root, m.To) {
|
||||
bad(i, m, "the local search agent's own folder (.agent) is never moved from or into")
|
||||
continue
|
||||
}
|
||||
if dcnExcludedAsSent(excludes, root, m.From) {
|
||||
bad(i, m, "source is excluded by the grant")
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
package main
|
||||
|
||||
// Two things a host holding `read` on a folder must never reach through the
|
||||
// file ops (list, stat, read_range), nor move with a plan:
|
||||
// - what an exclude of several folders names. The Host's default excludes
|
||||
// carry ".config/gcloud", and a pattern with a "/" was compared with one
|
||||
// folder name at a time, so it matched nothing: a shared home folder
|
||||
// served its gcloud credentials;
|
||||
// - the local search agent's own folder, .agent, which holds the plaintext
|
||||
// of every file the agent read, whatever the grant excludes.
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// dcnDefaultExcludes is the exclude list the Host proposes for a new folder.
|
||||
var dcnDefaultExcludes = []string{
|
||||
".ssh", ".gnupg", ".aws", ".azure", ".config/gcloud", ".kube",
|
||||
".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "*.keystore",
|
||||
"id_rsa*", "id_ed25519*", "*.token", "credentials*", ".netrc",
|
||||
".git-credentials", "*history",
|
||||
}
|
||||
|
||||
// syncExcluding installs a read grant for the store with these excludes,
|
||||
// through the real grant_sync op.
|
||||
func (fx *dcnStoreFixture) syncExcluding(t *testing.T, excludes []string) {
|
||||
t.Helper()
|
||||
rec := signedRec(t, fx.priv, map[string]any{"kind": "dcn_store_grant", "class": "machine",
|
||||
"root": fx.store, "mask": []string{"read"}, "excludes": excludes,
|
||||
"expires": time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339)})
|
||||
payload, _ := json.Marshal(map[string]any{"store_id": 1, "terms_canonical": rec.TermsCanonical,
|
||||
"signature": rec.Signature, "verify_pubkey": base64.StdEncoding.EncodeToString(fx.pub)})
|
||||
_, why, exit := dcnRunOp(t, func(ws *websocket.Conn) {
|
||||
handleDcnGrantSync(ws, fx.dir, []string{base64.StdEncoding.EncodeToString(payload)})
|
||||
})
|
||||
if exit != 0 {
|
||||
t.Fatalf("grant_sync: exit %d: %s", exit, why)
|
||||
}
|
||||
}
|
||||
|
||||
func (fx *dcnStoreFixture) put(t *testing.T, rel, body string) {
|
||||
t.Helper()
|
||||
p := filepath.Join(fx.store, filepath.FromSlash(rel))
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// listed is what dcn_list returns under prefix, and its exit code.
|
||||
func (fx *dcnStoreFixture) listed(t *testing.T, prefix string) ([]string, int) {
|
||||
t.Helper()
|
||||
obj, _, exit := fx.op(t, handleDcnList, map[string]any{"prefix": prefix})
|
||||
var paths []string
|
||||
files, _ := obj["files"].([]any)
|
||||
for _, f := range files {
|
||||
if m, ok := f.(map[string]any); ok {
|
||||
p, _ := m["path"].(string)
|
||||
paths = append(paths, p)
|
||||
}
|
||||
}
|
||||
return paths, exit
|
||||
}
|
||||
|
||||
// A pattern with a "/" names that run of folders, wherever it sits in the
|
||||
// path, each folder compared as the file system compares names. Judged per
|
||||
// platform from any OS.
|
||||
func TestDcnAMultiFolderExcludeMatchesThatRunAnywhere(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
goos, path string
|
||||
want bool
|
||||
}{
|
||||
{"linux", ".config/gcloud/access_tokens.db", true},
|
||||
{"linux", ".config/gcloud", true},
|
||||
{"linux", "home/.config/gcloud/x", true},
|
||||
{"linux", "a/b/.config/gcloud/configurations/config_default", true},
|
||||
{"linux", ".config/gcloudx", false},
|
||||
{"linux", ".config/gcloudx/access_tokens.db", false},
|
||||
{"linux", ".configx/gcloud/x", false},
|
||||
{"linux", ".config/other/gcloud/x", false},
|
||||
{"linux", "gcloud/.config/x", false},
|
||||
{"linux", ".config", false},
|
||||
{"linux", ".config/settings.ini", false},
|
||||
{"linux", ".Config/gcloud/x", false}, // another folder on Linux
|
||||
{"darwin", ".Config/GCloud/access_tokens.db", true},
|
||||
{"darwin", "home/.CONFIG/gcloud/x", true},
|
||||
{"darwin", ".config/gcloudx/x", false},
|
||||
{"windows", ".CONFIG/GCLOUD/access_tokens.db", true},
|
||||
{"windows", "home/.config./gcloud /x", true},
|
||||
{"windows", ".config/gcloudx/x", false},
|
||||
// one-folder patterns as before
|
||||
{"linux", "deep/.ssh/config", true},
|
||||
{"linux", "certs/server.key", true},
|
||||
{"linux", "notes/ssh-notes.txt", false},
|
||||
{"windows", ".SSH/id_rsa", true},
|
||||
} {
|
||||
if got := dcnExcludedOn(c.goos, dcnDefaultExcludes, c.path); got != c.want {
|
||||
t.Errorf("%s: %q excluded=%v, want %v", c.goos, c.path, got, c.want)
|
||||
}
|
||||
}
|
||||
// A pattern's own slashes at either end, or doubled, name the same run.
|
||||
for _, pat := range []string{".config/gcloud/", "/.config/gcloud", ".config//gcloud"} {
|
||||
if !dcnExcludedOn("linux", []string{pat}, "home/.config/gcloud/x") {
|
||||
t.Errorf("%q must exclude home/.config/gcloud/x", pat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The default exclude of gcloud's folder holds on every op a host sends, at the
|
||||
// top of a home folder and inside one, and however the path is spelled.
|
||||
func TestDcnTheDefaultGcloudExcludeHoldsOnEveryOp(t *testing.T) {
|
||||
fx := newDcnStoreFixture(t)
|
||||
fx.put(t, ".config/gcloud/access_tokens.db", "GCLOUD-TOKEN")
|
||||
fx.put(t, "home/.config/gcloud/credentials.db", "NESTED-TOKEN")
|
||||
fx.put(t, ".config/gcloudx/ok.txt", "NEAR-MISS")
|
||||
fx.put(t, ".config/settings.ini", "SETTINGS")
|
||||
fx.syncExcluding(t, dcnDefaultExcludes)
|
||||
|
||||
paths, exit := fx.listed(t, "")
|
||||
if exit != 0 {
|
||||
t.Fatalf("list: exit %d", exit)
|
||||
}
|
||||
have := map[string]bool{}
|
||||
for _, p := range paths {
|
||||
have[p] = true
|
||||
if strings.Contains(p, ".config/gcloud/") {
|
||||
t.Errorf("list returned %q, under an excluded folder", p)
|
||||
}
|
||||
}
|
||||
for _, p := range []string{".config/gcloudx/ok.txt", ".config/settings.ini", "a.txt"} {
|
||||
if !have[p] {
|
||||
t.Errorf("list left out %q, which no exclude names (got %v)", p, paths)
|
||||
}
|
||||
}
|
||||
if paths, exit := fx.listed(t, "home/.config"); exit != 0 || len(paths) != 0 {
|
||||
t.Errorf("list under home/.config: exit %d, %v; want nothing", exit, paths)
|
||||
}
|
||||
for _, prefix := range []string{".config/gcloud", "home/.config/gcloud"} {
|
||||
if _, exit := fx.listed(t, prefix); exit != 126 {
|
||||
t.Errorf("list of %q: exit %d, want 126", prefix, exit)
|
||||
}
|
||||
}
|
||||
for _, p := range []string{".config/gcloud", ".config/gcloud/access_tokens.db",
|
||||
"home/.config/gcloud/credentials.db", " .config/gcloud/access_tokens.db",
|
||||
"docs/../.config/gcloud/access_tokens.db"} {
|
||||
if obj, _, exit := fx.op(t, handleDcnStat, map[string]any{"path": p}); exit != 126 {
|
||||
t.Errorf("stat %q: exit %d %v, want 126", p, exit, obj)
|
||||
}
|
||||
if got, _, exit := fx.read(t, p); exit != 126 || strings.Contains(got, "TOKEN") {
|
||||
t.Errorf("read_range %q: exit %d %q, want 126 and nothing", p, exit, got)
|
||||
}
|
||||
}
|
||||
if got, why, exit := fx.read(t, ".config/gcloudx/ok.txt"); exit != 0 || got != "NEAR-MISS" {
|
||||
t.Errorf("a folder only named like gcloud's must read: exit %d %q %s", exit, got, why)
|
||||
}
|
||||
if problems := dcnCheckPlan(fx.store, dcnDefaultExcludes, []dcnMove{
|
||||
{From: ".config/gcloud/access_tokens.db", To: "out.db"}}); len(problems) == 0 {
|
||||
t.Error("a move out of gcloud's folder must be refused")
|
||||
}
|
||||
if problems := dcnCheckPlan(fx.store, dcnDefaultExcludes, []dcnMove{
|
||||
{From: "a.txt", To: "home/.config/gcloud/a.txt"}}); len(problems) == 0 {
|
||||
t.Error("a move into gcloud's folder must be refused")
|
||||
}
|
||||
if err := os.Symlink(filepath.Join(".config", "gcloud"), filepath.Join(fx.store, "gc")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
if got, _, exit := fx.read(t, "gc/access_tokens.db"); exit != 126 || got == "GCLOUD-TOKEN" {
|
||||
t.Errorf("read through a link to gcloud's folder: exit %d %q, want 126", exit, got)
|
||||
}
|
||||
}
|
||||
|
||||
// The agent's folder is refused on every op a host sends: at the store's top,
|
||||
// inside a folder another grant shares (where that store's agent keeps its
|
||||
// own), through a link, and however it is spelled. The grant here excludes
|
||||
// nothing, so the refusal is not the grant's to give or take.
|
||||
func TestDcnTheAgentsFolderIsNeverServed(t *testing.T) {
|
||||
fx := newDcnStoreFixture(t)
|
||||
fx.put(t, ".agent/extract/ab/report.txt", "PLAINTEXT-OF-A-FILE")
|
||||
fx.put(t, ".agent/manifest.yaml", "manifest")
|
||||
fx.put(t, "proj/.agent/extract/x.txt", "NESTED-PLAINTEXT")
|
||||
fx.put(t, "proj/readme.md", "README")
|
||||
fx.syncExcluding(t, nil)
|
||||
|
||||
paths, exit := fx.listed(t, "")
|
||||
if exit != 0 {
|
||||
t.Fatalf("list: exit %d", exit)
|
||||
}
|
||||
have := map[string]bool{}
|
||||
for _, p := range paths {
|
||||
have[p] = true
|
||||
if strings.Contains("/"+p, "/.agent/") {
|
||||
t.Errorf("list returned %q, inside an agent's folder", p)
|
||||
}
|
||||
}
|
||||
if !have["a.txt"] || !have["proj/readme.md"] {
|
||||
t.Errorf("list left out the folder's own files: %v", paths)
|
||||
}
|
||||
for _, prefix := range []string{".agent", ".agent/extract", "proj/.agent"} {
|
||||
if _, exit := fx.listed(t, prefix); exit != 126 {
|
||||
t.Errorf("list of %q: exit %d, want 126", prefix, exit)
|
||||
}
|
||||
}
|
||||
for _, p := range []string{".agent", ".agent/extract/ab/report.txt", ".agent/manifest.yaml",
|
||||
"proj/.agent/extract/x.txt", " .agent/manifest.yaml", "docs/../.agent/extract/ab/report.txt"} {
|
||||
if obj, _, exit := fx.op(t, handleDcnStat, map[string]any{"path": p}); exit != 126 {
|
||||
t.Errorf("stat %q: exit %d %v, want 126", p, exit, obj)
|
||||
}
|
||||
if got, _, exit := fx.read(t, p); exit != 126 || strings.Contains(got, "PLAINTEXT") {
|
||||
t.Errorf("read_range %q: exit %d %q, want 126 and nothing", p, exit, got)
|
||||
}
|
||||
}
|
||||
for _, m := range []dcnMove{{From: ".agent/extract/ab/report.txt", To: "out.txt"},
|
||||
{From: "a.txt", To: ".agent/a.txt"}, {From: "a.txt", To: "proj/.agent/a.txt"}} {
|
||||
if problems := dcnCheckPlan(fx.store, nil, []dcnMove{m}); len(problems) == 0 {
|
||||
t.Errorf("a move %q -> %q must be refused", m.From, m.To)
|
||||
}
|
||||
}
|
||||
|
||||
// The bastion's own report of a plan it applied still goes into
|
||||
// .agent/plans, through the real plan op.
|
||||
hn, _ := os.Hostname()
|
||||
rec := signedRec(t, fx.priv, map[string]any{"kind": "dcn_move_plan", "store_id": 1, "plan_id": 41,
|
||||
"server": hn, "root": fx.store, "archive": "Archive",
|
||||
"moves": []map[string]any{{"from": "a.txt", "to": "Archive/a.txt"}},
|
||||
"expires": time.Now().Add(time.Hour).UTC().Format(time.RFC3339)})
|
||||
payload, _ := json.Marshal(map[string]any{"terms_canonical": rec.TermsCanonical,
|
||||
"signature": rec.Signature, "mode": "apply"})
|
||||
out, why, exit := dcnRunOp(t, func(ws *websocket.Conn) {
|
||||
handleDcnPlanApply(ws, fx.dir, []string{"1", base64.StdEncoding.EncodeToString(payload)})
|
||||
})
|
||||
if exit != 0 || !strings.Contains(out, `"ok"`) {
|
||||
t.Fatalf("plan apply: exit %d %s %s", exit, out, why)
|
||||
}
|
||||
if b, err := os.ReadFile(filepath.Join(fx.store, ".agent", "plans", "41.json")); err != nil ||
|
||||
!strings.Contains(string(b), `"state": "ok"`) {
|
||||
t.Fatalf("the plan's report was not written into .agent/plans: %q %v", b, err)
|
||||
}
|
||||
|
||||
if err := os.Symlink(filepath.Join(".agent", "extract", "ab", "report.txt"), filepath.Join(fx.store, "notes.txt")); err != nil {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
if err := os.Symlink(".agent", filepath.Join(fx.store, "ag")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range []string{"notes.txt", "ag/extract/ab/report.txt", "ag/manifest.yaml"} {
|
||||
if got, _, exit := fx.read(t, p); exit != 126 || strings.Contains(got, "PLAINTEXT") {
|
||||
t.Errorf("read_range %q, a link into .agent: exit %d %q, want 126", p, exit, got)
|
||||
}
|
||||
}
|
||||
if _, exit := fx.listed(t, "ag"); exit != 126 {
|
||||
t.Errorf("list of a link to .agent: exit %d, want 126", exit)
|
||||
}
|
||||
}
|
||||
|
||||
// The agent's folder is named as the file system names it, and on every OS
|
||||
// regardless of case, as the agent's own walker treats it: a ".Agent" left on
|
||||
// a volume that ignores case is the agent's folder.
|
||||
func TestDcnTheAgentsFolderIsMatchedHoweverTheNameIsCased(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
goos, path string
|
||||
want bool
|
||||
}{
|
||||
{"linux", ".agent/extract/x", true},
|
||||
{"linux", ".AGENT/extract/x", true},
|
||||
{"darwin", ".Agent/vectors.db", true},
|
||||
{"windows", ".AGENT/extract/x", true},
|
||||
{"windows", ".agent./extract/x", true},
|
||||
{"windows", ".agent /extract/x", true},
|
||||
{"windows", ".agent::$INDEX_ALLOCATION/x", true},
|
||||
{"linux", "deep/.agent", true},
|
||||
{"linux", ".agentx/x", false},
|
||||
{"linux", "agent/x", false},
|
||||
{"linux", "notes/.agent-old/x", false},
|
||||
{"darwin", "docs/readme.md", false},
|
||||
} {
|
||||
if got := dcnExcludedOn(c.goos, nil, c.path); got != c.want {
|
||||
t.Errorf("%s: %q refused=%v, want %v", c.goos, c.path, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-5
@@ -68,17 +68,22 @@ type freeagentConfig struct {
|
||||
|
||||
// faManagedSanitizeMode is the sanitize mode the bastion writes into every
|
||||
// manifest it deploys (faManifestText). Flag mode is the agent's rules pass
|
||||
// only: it replaces e-mail addresses, phone numbers, card, bank and ID
|
||||
// numbers, IP and MAC addresses and keys, and does not find names, street
|
||||
// addresses or dates of birth.
|
||||
// only (the agent's pii/detect.py): it replaces what a pattern can find
|
||||
// (faSanitizeNotes["flag"] names each kind), and does not look for names,
|
||||
// street addresses, dates of birth, ID numbers such as passport numbers, or
|
||||
// bank account numbers other than IBANs.
|
||||
const faManagedSanitizeMode = "flag"
|
||||
|
||||
// faSanitizeNotes says, for each mode a reply can carry, what personal data
|
||||
// may still be in the text, in the words the host relays. A mode with no entry
|
||||
// here is reported as not known to remove personal data.
|
||||
var faSanitizeNotes = map[string]string{
|
||||
"flag": "rules only: e-mail addresses, phone numbers, card, bank and ID numbers, IP and MAC addresses " +
|
||||
"and keys were replaced; names, street addresses and dates of birth were not",
|
||||
"flag": "rules only. Replaced: e-mail addresses, phone numbers, card numbers, US social security numbers, " +
|
||||
"international bank account numbers (IBANs), US bank routing numbers written after a word like " +
|
||||
"\"routing\", Bitcoin addresses that start with 1 or 3, IP and MAC addresses, private keys, and " +
|
||||
"access keys and tokens of the kinds it knows. Other numbers that look like one of these can be " +
|
||||
"replaced too. Not looked for, so they can still be in the text: names, street addresses, dates of " +
|
||||
"birth, passport, licence and other ID numbers, other bank account numbers and other crypto addresses",
|
||||
"sterilize": "",
|
||||
"none": "the agent's manifest has no sanitize stage, so nothing in the text was replaced",
|
||||
}
|
||||
|
||||
@@ -330,8 +330,16 @@ func TestASanitizeStampNamesTheModeThatRan(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if !strings.Contains(faSanitizeNotes["flag"], "names") {
|
||||
t.Fatal("the flag note must say names are left in")
|
||||
// The agent's rules pass (pii/detect.py) does not look for these, so the
|
||||
// note the host relays must say they can still be there.
|
||||
for _, left := range []string{"names", "street addresses", "dates of birth", "passport",
|
||||
"other bank account numbers", "other crypto addresses"} {
|
||||
if !strings.Contains(faSanitizeNotes["flag"], left) {
|
||||
t.Errorf("the flag note must say %s can still be in the text: %q", left, faSanitizeNotes["flag"])
|
||||
}
|
||||
}
|
||||
if strings.Contains(faSanitizeNotes["flag"], "ID numbers, IP") || strings.Contains(faSanitizeNotes["flag"], "bank and ID") {
|
||||
t.Errorf("the flag note says ID or bank numbers were replaced, which the rules pass does not do: %q", faSanitizeNotes["flag"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,13 +137,14 @@ func landsInside(root, p string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// excludedOnDisk is the fixture's excludes (".ssh", "*.key") judged
|
||||
// excludedOnDisk is the fixture's excludes (".ssh", "*.key"), and the local
|
||||
// search agent's folder (".agent", which no grant reaches), judged
|
||||
// independently of dcnExcluded: on the path relative to the root after every
|
||||
// link is followed, and on the text of the path the sandbox opens.
|
||||
func excludedOnDisk(root, p string) bool {
|
||||
check := func(rel string) bool {
|
||||
for _, seg := range strings.Split(rel, "/") {
|
||||
if seg == ".ssh" || strings.HasSuffix(seg, ".key") {
|
||||
if seg == ".ssh" || strings.HasSuffix(seg, ".key") || strings.EqualFold(seg, ".agent") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
+10
-8
@@ -2,12 +2,13 @@ package main
|
||||
|
||||
// Git lane: Gitea through the Host.
|
||||
//
|
||||
// git.privatae.ai answers only platform/DMZ addresses now, so the user's LOCAL git
|
||||
// cannot reach it. This bastion offers a loopback HTTP proxy for git; every local
|
||||
// connection is spliced over the tunnel into ONE WebSocket on the host (/bastion-git,
|
||||
// same bearer + mTLS binding as the other lanes) and the host relays it out the
|
||||
// platform path to git.privatae.ai ONLY. The wire framing is the egress lane's
|
||||
// ('D'+bytes / 'E' end) with the roles swapped, so wsConn is reused unchanged.
|
||||
// This bastion offers a loopback HTTP proxy for the user's LOCAL git to reach
|
||||
// git.privatae.ai through their host; every local connection is spliced over the
|
||||
// tunnel into ONE WebSocket on the host (/bastion-git, same bearer + mTLS binding
|
||||
// as the other lanes) and the host relays it on to the forge. Which addresses the
|
||||
// forge itself answers is the forge's rule; nothing here sets or checks it. The
|
||||
// wire framing is the egress lane's ('D'+bytes / 'E' end) with the roles swapped,
|
||||
// so wsConn is reused unchanged.
|
||||
//
|
||||
// Setup for the user (printed by `privatae-bastion git-setup`):
|
||||
// git config --global http.https://git.privatae.ai.proxy http://127.0.0.1:<port>
|
||||
@@ -199,7 +200,8 @@ func gitCredentialReply(body []byte, authtype bool) []byte {
|
||||
return []byte("username=" + user + "\npassword=" + pass + "\nquit=1\n")
|
||||
}
|
||||
|
||||
// cmdGitSetup prints the two git config lines for every pairing.
|
||||
// cmdGitSetup prints the three git config lines for every pairing: the proxy,
|
||||
// the reset of the forge's helper chain, and the helper.
|
||||
func cmdGitSetup() error {
|
||||
all, err := loadPairings()
|
||||
if err != nil {
|
||||
@@ -208,7 +210,7 @@ func cmdGitSetup() error {
|
||||
if len(all) == 0 {
|
||||
return fmt.Errorf("no pairings — run `privatae-bastion pair <token>` first")
|
||||
}
|
||||
fmt.Println("git through your bastion (git.privatae.ai is reachable only this way):")
|
||||
fmt.Println("git to git.privatae.ai through this bastion and your host:")
|
||||
if len(all) > 1 {
|
||||
fmt.Println("(several hosts are paired: pick ONE for git, and name it with --host in the helper line)")
|
||||
}
|
||||
|
||||
+2
-2
@@ -142,8 +142,8 @@ Usage:
|
||||
let your hosts run sealed coding runs on this machine (Linux, as
|
||||
root): how many at once, how big each is, which engines. Off by
|
||||
default; each run has no network but a proxy to the internet
|
||||
privatae-bastion git-setup print the two git config lines that route git.privatae.ai
|
||||
through this bastion (the forge is reachable only this way)
|
||||
privatae-bastion git-setup print the git config lines that route git.privatae.ai
|
||||
through this bastion and your host
|
||||
privatae-bastion git-credential git's credential helper — fetches your forge token from your
|
||||
host on every git auth (git-setup resets the helper chain so no
|
||||
other helper caches it)
|
||||
|
||||
+3
-3
@@ -533,9 +533,9 @@ func runPairing(ctx context.Context, cfg *Config) error {
|
||||
}()
|
||||
|
||||
// 2d. Git lane: loopback HTTP proxy for git.privatae.ai only.
|
||||
// Each local connection = one WebSocket to the host, relayed out the PLATFORM
|
||||
// path there (the forge answers platform/DMZ addresses only). No credential
|
||||
// rides this lane; git presents its own via the git-credential helper below.
|
||||
// Each local connection = one WebSocket to the host, which relays it on to
|
||||
// the forge (gitlane.go). No credential rides this lane; git presents its
|
||||
// own via the git-credential helper below.
|
||||
if cfg.GitProxyAddr != "" {
|
||||
if gitLn, err := net.Listen("tcp", cfg.GitProxyAddr); err != nil {
|
||||
// Optional lane: a port collision must not take the tunnel, lanes and
|
||||
|
||||
Reference in New Issue
Block a user