Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ SAM provides the open protocols, cryptographic building blocks, and software to
1. **Deploy Dedicated Mesh Infrastructure:** Run a dedicated control plane (`sam-control-plane`) and routing relays (`sam-router`) on your chosen infrastructure (managed cloud environments like Google Cloud, private Kubernetes clusters, or air-gapped datacenters) using our [Helm chart](charts/sam-mesh/README.md) or Kubernetes manifests.
2. **Maintain Root Cryptographic Key Custody:** Generate, manage, and hold your own Ed25519 root signing keys (via local HSMs, KMS, or Cloud EKM). You maintain 100% of the cryptographic authority—no external party can mint credentials, revoke nodes, or alter policies.
3. **Bring Your Own Identity Provider:** Bridge agent and user identities through your own OIDC identity provider (such as Dex, Keycloak, or corporate IdP).
4. **Enforce Territorial & Jurisdictional Boundaries:** Use cryptographically attested label gates (`--labels jurisdiction=eu`, `X-Sam-Required-Labels`) to mathematically guarantee prompts and tool invocations never leave authorized geographic scopes.
4. **Enforce Territorial & Jurisdictional Boundaries:** Use cryptographically attested label gates (`labels: {jurisdiction: eu}` in the node config, `X-Sam-Required-Labels`) to mathematically guarantee prompts and tool invocations never leave authorized geographic scopes.
5. **Retain Autonomous Local Vetoes:** Configure local node attenuation policies (`sam-node.yaml`) to evaluate access rules *before* control plane grants, ensuring local nodes retain absolute veto authority.

> [!NOTE]
Expand Down
7 changes: 4 additions & 3 deletions api/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ type ServiceConfig struct {

// NodeConfig defines the optional attenuation rules and static services for a specific SAM Node.
type NodeConfig struct {
Version string `yaml:"version"`
Attenuation Attenuation `yaml:"attenuation"`
Services []ServiceConfig `yaml:"services"`
Version string `yaml:"version"`
Attenuation Attenuation `yaml:"attenuation"`
Services []ServiceConfig `yaml:"services"`
Labels map[string]string `yaml:"labels,omitempty"`
}

// NodeConfigVersionV1Alpha1 is the only node config schema this build understands.
Expand Down
5 changes: 3 additions & 2 deletions charts/sam-mesh/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ bootstrap:
# until a policy grants it (fail closed); never use ["*"] outside an
# intentionally public mesh.
nodeServices: []
# Label patterns ("key=value" or "key=*") sam:role:node may attest with
# --labels. Empty means nodes cannot enroll with any label (fail closed).
# Label patterns ("key=value" or "key=*") sam:role:node may attest with the
# labels in its sam-node.yaml. Empty means nodes cannot enroll with any label
# (fail closed).
nodeLabels: []
3 changes: 3 additions & 0 deletions charts/sam-node/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ extraArgs: []
# advertises, and local attenuation.
config:
version: v1alpha1
# Operator-declared labels the control plane attests, subject to the role's
# allowed_labels grant (e.g. region: us-east-1).
labels: {}
attenuation:
policies: []
checks: []
Expand Down
2 changes: 1 addition & 1 deletion cmd/sam-node/daemonize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestWithoutDaemonizeFlag(t *testing.T) {
}

// A value that merely looks like the flag must survive.
got = withoutDaemonizeFlag([]string{"run", "--labels", "mode=--daemonize"})
got = withoutDaemonizeFlag([]string{"run", "--log-level", "mode=--daemonize"})
if len(got) != 3 {
t.Errorf("flag values must not be stripped: got %v", got)
}
Expand Down
40 changes: 0 additions & 40 deletions cmd/sam-node/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ var (
apiTokenPathFlag string
bootstrapTokenPathFlag string
clientSecretPathFlag string
labelsFlag string
tlsCertFlag string
tlsKeyFlag string
tlsCAFlag string
Expand Down Expand Up @@ -259,31 +258,6 @@ func interactiveJoin(ctx context.Context, store *node.Store, targetControlPlane
return jwtStr, info, nil
}

// parseLabelsFlag parses a comma-separated "key=value" list (see
// api/labels.go) into a label map; an empty string means no claims.
func parseLabelsFlag(s string) (map[string]string, error) {
if s == "" {
return nil, nil
}
labels := make(map[string]string)
for _, part := range strings.Split(s, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
k, v, ok := strings.Cut(part, "=")
if !ok {
return nil, fmt.Errorf("invalid label %q: expected key=value", part)
}
key := strings.TrimSpace(k)
if _, exists := labels[key]; exists {
return nil, fmt.Errorf("duplicate label key %q", key)
}
labels[key] = strings.TrimSpace(v)
}
return labels, nil
}

func main() {
rootCmd := &cobra.Command{
Use: "sam-node",
Expand Down Expand Up @@ -327,10 +301,6 @@ func main() {
if jwtFlag != "" {
logger.Warn("--jwt passes a secret on the command line; prefer --jwt-path")
}
labels, err := parseLabelsFlag(labelsFlag)
if err != nil {
logger.Fatalf("Invalid --labels: %v", err)
}

store, err := node.NewStore(resolveDataDir())
if err != nil {
Expand Down Expand Up @@ -491,7 +461,6 @@ func main() {
AutoRelayBackoff: autoRelayBackoffFlag,
RouterConnectTimeout: routerConnectTimeoutFlag,
RequiredRole: api.RoleNode,
Labels: labels,
PolicySyncInterval: policySyncIntervalFlag,
DHTProviderAddrTTL: dhtProviderAddrTTLFlag,
DHTMaxRecordAge: dhtMaxRecordAgeFlag,
Expand Down Expand Up @@ -559,7 +528,6 @@ func main() {
AutoRelayBackoff: autoRelayBackoffFlag,
RouterConnectTimeout: routerConnectTimeoutFlag,
RequiredRole: api.RoleNode,
Labels: labels,
PolicySyncInterval: policySyncIntervalFlag,
DHTProviderAddrTTL: dhtProviderAddrTTLFlag,
DHTMaxRecordAge: dhtMaxRecordAgeFlag,
Expand Down Expand Up @@ -630,7 +598,6 @@ func main() {
AutoRelayBackoff: autoRelayBackoffFlag,
RouterConnectTimeout: routerConnectTimeoutFlag,
RequiredRole: api.RoleNode,
Labels: labels,
PolicySyncInterval: policySyncIntervalFlag,
})
if err != nil {
Expand Down Expand Up @@ -746,11 +713,6 @@ func main() {
}
}

labels, err := parseLabelsFlag(labelsFlag)
if err != nil {
logger.Fatalf("Invalid --labels: %v", err)
}

priv := node.GetOrGenerateKey(store)
meshNode, err := node.NewSamNode(node.Options{
PrivKey: priv,
Expand All @@ -771,7 +733,6 @@ func main() {
AutoRelayBackoff: 3 * time.Second,
RouterConnectTimeout: routerConnectTimeoutFlag,
RequiredRole: api.RoleNode,
Labels: labels,
PolicySyncInterval: policySyncIntervalFlag,
})
if err != nil {
Expand Down Expand Up @@ -885,7 +846,6 @@ func main() {
joinCmd.Flags().StringVar(&bootstrapTokenFlag, "bootstrap-token", "", "Pre-shared bootstrap token for enrollment")
joinCmd.Flags().StringVar(&bootstrapTokenPathFlag, "bootstrap-token-path", "", "Path to file containing the bootstrap token (recommended over --bootstrap-token)")
runCmd.Flags().StringVar(&apiTokenPathFlag, "api-token-path", "", "Path to file containing the static Bearer token for API authorization (or env SAM_API_TOKEN)")
runCmd.Flags().StringVar(&labelsFlag, "labels", "", "Operator-declared key=value labels of this node, comma-separated (e.g. \"region=us-east-1,team=platform\"); empty means no claims")
runCmd.Flags().StringVar(&tlsCertFlag, "tls-cert", "", "Path to TLS certificate for sidecar API")
runCmd.Flags().StringVar(&tlsKeyFlag, "tls-key", "", "Path to TLS key for sidecar API")
runCmd.Flags().StringVar(&tlsCAFlag, "tls-ca", "", "Path to TLS CA for sidecar API mTLS")
Expand Down
19 changes: 0 additions & 19 deletions cmd/sam-node/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,25 +62,6 @@ func TestResolveSocketPath(t *testing.T) {
})
}

func TestParseLabelsFlag(t *testing.T) {
if got, err := parseLabelsFlag(""); got != nil || err != nil {
t.Errorf("empty flag: got %v, %v; want nil, nil", got, err)
}

got, err := parseLabelsFlag(" region=eu , team=platform ,,")
if err != nil || len(got) != 2 || got["region"] != "eu" || got["team"] != "platform" {
t.Errorf("parse should split key=value pairs: got %v, %v", got, err)
}

if _, err := parseLabelsFlag("noequals"); err == nil {
t.Error("entry without '=' must be rejected")
}

if _, err := parseLabelsFlag("region=us-east-1,region=us-west-1"); err == nil {
t.Error("duplicate label key must be rejected")
}
}

func TestNormalizeControlPlaneURL(t *testing.T) {
cases := map[string]string{
"bananas.sam-mesh.dev": "https://bananas.sam-mesh.dev",
Expand Down
8 changes: 8 additions & 0 deletions internal/node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type NodeConfigComplete struct {
Checks []biscuit.Check
Rules []biscuit.Rule
Services []api.ServiceConfig
Labels map[string]string
}

// LoadNodeConfig loads the node configuration from the specified path.
Expand All @@ -56,8 +57,15 @@ func LoadNodeConfig(path string) (*NodeConfigComplete, error) {
path, config.Version, api.NodeConfigVersionV1Alpha1)
}

// The control plane attests this set at enrollment, so a malformed label
// must stop the node here rather than surface as a refused enrollment.
if err := api.ValidateLabels(config.Labels); err != nil {
return nil, fmt.Errorf("invalid node config %s: %w", path, err)
}

complete := &NodeConfigComplete{
Services: config.Services,
Labels: config.Labels,
}

for i, svc := range config.Services {
Expand Down
29 changes: 29 additions & 0 deletions internal/node/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,35 @@ attenuation:
- 'deny if user("bob");'
policiez:
- 'deny if user("alice");'
`,
wantErr: true,
},
{
name: "Labels are parsed",
yamlContent: `
version: "v1alpha1"
labels:
region: "us-east-1"
team: "platform"
`,
wantErr: false,
verify: func(t *testing.T, config *NodeConfigComplete) {
if len(config.Labels) != 2 {
t.Fatalf("got %d labels, want 2: %v", len(config.Labels), config.Labels)
}
if config.Labels["region"] != "us-east-1" || config.Labels["team"] != "platform" {
t.Errorf("unexpected labels: %v", config.Labels)
}
},
},
{
// The label set is what the control plane attests, so a malformed
// one must stop the node at load rather than at enrollment.
name: "Malformed label key is rejected",
yamlContent: `
version: "v1alpha1"
labels:
"bad key!": "us-east-1"
`,
wantErr: true,
},
Expand Down
2 changes: 1 addition & 1 deletion internal/node/discovery_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func capKeys(keys []string) []string {
// discoverySource builds gossip announcements from the registered services:
// inference services announce model IDs, MCP services announce tool names.
func (n *SamNode) discoverySource() []discovery.Announcement {
labels := n.config.Labels // validated at startup
labels := n.labels()
var out []discovery.Announcement
for _, info := range n.services.List(api.ServiceType_SERVICE_TYPE_UNSPECIFIED) {
svc, ok := n.services.Get(info.GetName())
Expand Down
4 changes: 2 additions & 2 deletions internal/node/enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (n *SamNode) enrollHTTP(ctx context.Context, controlPlaneURL, jwt string, p
PeerId: peerID.String(),
PublicKey: pubBytes,
RequestedRole: n.config.RequiredRole,
Labels: n.config.Labels, // validated at startup
Labels: n.labels(),
}
data, err := proto.Marshal(req)
if err != nil {
Expand Down Expand Up @@ -234,7 +234,7 @@ func (n *SamNode) EnrollBootstrap(ctx context.Context, controlPlaneURL string, b
PeerId: n.Host.ID().String(),
PublicKey: pubBytes,
RequestedRole: n.config.RequiredRole,
Labels: n.config.Labels, // validated at startup
Labels: n.labels(),
Timestamp: enrollTS,
ChallengeSignature: enrollSig,
}
Expand Down
4 changes: 2 additions & 2 deletions internal/node/mcp_discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ func (f *fakeToolService) Tools(_ context.Context) ([]string, error) {

func TestDiscoverySource(t *testing.T) {
node := &SamNode{
services: NewServiceRegistry(&fakeDHT{}),
config: Options{Labels: map[string]string{"region": "EU"}},
services: NewServiceRegistry(&fakeDHT{}),
nodeConfig: &NodeConfigComplete{Labels: map[string]string{"region": "EU"}},
}
ctx := context.Background()

Expand Down
8 changes: 4 additions & 4 deletions internal/node/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,14 @@ func (n *SamNode) Authorize(rawToken []byte, req RequestContext, pubKey ed25519.
return fmt.Errorf("failed to inject target facts: %w", err)
}

if n.LocalPolicy != nil {
for _, p := range n.LocalPolicy.Policies {
if n.nodeConfig != nil {
for _, p := range n.nodeConfig.Policies {
authorizer.AddPolicy(p)
}
for _, c := range n.LocalPolicy.Checks {
for _, c := range n.nodeConfig.Checks {
authorizer.AddCheck(c)
}
for _, r := range n.LocalPolicy.Rules {
for _, r := range n.nodeConfig.Rules {
authorizer.AddRule(r)
}
}
Expand Down
6 changes: 3 additions & 3 deletions internal/node/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,23 +582,23 @@ attenuation:
t.Fatal(err)
}

var localPolicy *NodeConfigComplete
var nodeConfigComplete *NodeConfigComplete
if tt.localPolicyYAML != "" {
dir := t.TempDir()
policyFile := filepath.Join(dir, "local_policy.yaml")
if err := os.WriteFile(policyFile, []byte(tt.localPolicyYAML), 0644); err != nil {
t.Fatal(err)
}
var err error
localPolicy, err = LoadNodeConfig(policyFile)
nodeConfigComplete, err = LoadNodeConfig(policyFile)
if err != nil {
t.Fatalf("failed to load local policy: %v", err)
}
}

node := &SamNode{
trustedKeys: []TrustedKey{{Key: pub, ReceivedAt: time.Now()}},
LocalPolicy: localPolicy,
nodeConfig: nodeConfigComplete,
BiscuitTimeout: 500 * time.Millisecond,
}

Expand Down
14 changes: 12 additions & 2 deletions internal/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ type SamNode struct {
receivedMsgs map[string][]string
topics map[string]*pubsub.Topic
mu sync.Mutex
LocalPolicy *NodeConfigComplete
nodeConfig *NodeConfigComplete
revokedPeers *lru.Cache[string, int64]
peerLabelGate *lru.Cache[string, time.Time]
authPeers sync.Map
Expand Down Expand Up @@ -282,7 +282,7 @@ func NewSamNode(cfg Options) (*SamNode, error) {
receivedMsgs: make(map[string][]string),
topics: make(map[string]*pubsub.Topic),
authenticatedRouters: make(map[peer.ID]bool),
LocalPolicy: cfg.NodeConfig,
nodeConfig: cfg.NodeConfig,
AllowLoopback: cfg.AllowLoopback,
authSuccess: make(chan struct{}),
reprovideTrigger: make(chan struct{}, 1),
Expand Down Expand Up @@ -317,6 +317,16 @@ func NewSamNode(cfg Options) (*SamNode, error) {
return node, nil
}

// labels reports this node's operator-declared labels, validated at load.
// NewSamNode always leaves nodeConfig non-nil, but tests build SamNode
// literals directly, so the guard lives here rather than at each caller.
func (n *SamNode) labels() map[string]string {
if n.nodeConfig == nil {
return nil
}
return n.nodeConfig.Labels
}

// Start initializes the libp2p host, DHT, connects to the routers, and starts runtime components.
func (n *SamNode) Start(ctx context.Context) error {
if biscuitBytes := n.GetIdentity(); len(biscuitBytes) > 0 {
Expand Down
2 changes: 1 addition & 1 deletion internal/node/openai_facade.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func newOpenAIFacade(node *SamNode, egress http.Handler) *openAIFacade {
isRevoked: func(peerID string) bool {
return node.revokedPeers != nil && node.revokedPeers.Contains(peerID)
},
localLabels: func() map[string]string { return node.config.Labels },
localLabels: node.labels,
peerLabels: func(peerID string) map[string]string {
if node.Discovery == nil {
return nil
Expand Down
10 changes: 3 additions & 7 deletions internal/node/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,6 @@ type Options struct {
DiscoveryConcurrency int
// RequiredRole restricts enrollment and startup to only accept tokens containing this role.
RequiredRole string
// Labels are operator-declared key=value claims for this node (e.g.
// {"region": "us-east-1"}, see api/labels.go). Empty means no claims;
// consumers with a label requirement will not select this node.
Labels map[string]string
// PolicySyncInterval specifies how often the node syncs the mesh policy from the control plane.
PolicySyncInterval time.Duration
// PolicySyncJitter specifies the maximum jitter delay when scheduling policy syncs on event broadcasts.
Expand Down Expand Up @@ -135,6 +131,9 @@ func (o *Options) Default() {
if o.PolicySyncJitter <= 0 {
o.PolicySyncJitter = 10 * time.Second
}
if o.NodeConfig == nil {
o.NodeConfig = &NodeConfigComplete{}
}
}

// Validate verifies that the required options are provided and valid.
Expand All @@ -148,8 +147,5 @@ func (o *Options) Validate() error {
if o.RequiredRole == "" {
return fmt.Errorf("RequiredRole must be specified")
}
if err := api.ValidateLabels(o.Labels); err != nil {
return err
}
return nil
}
Loading
Loading