Phase 0a — ADRs (9 new): - ADR-026: Transport/interface separation (three-layer model) - ADR-027: Crate decomposition (core, secret, storage, flowgraph, napi, CLI) - ADR-028: Auth as irpc service (AuthProtocol behind feature flag) - ADR-029: Identity as core type (Identity + IdentityProvider in alknet-core) - ADR-030: Static/dynamic config split (ArcSwap, ConfigReloadHandle) - ADR-031: Forwarding policy (rule-based allow/deny, TransportKind-aware) - ADR-032: Event boundary discipline (domain, irpc, call protocol boundaries) - ADR-033: OperationEnv universal composition (three dispatch paths) - ADR-034: Head/worker terminology (replace hub/spoke) Phase 0b — New spec documents (7): - identity.md, services.md, interface.md, configuration.md, storage.md, flowgraph.md, secret-service.md Updated existing docs: - auth.md: reference identity.md for canonical definitions, add AuthProtocol - open-questions.md: resolve OQ-12, OQ-16, OQ-18, OQ-22, OQ-23-25 - README.md: add all new docs, ADRs 026-034 Marked 19 architecture tasks as completed.
5.1 KiB
ADR-031: Forwarding Policy
Status
Accepted
Context
Currently, any authenticated client can open a direct-tcpip SSH channel to
any destination. The only gate is authentication — once authenticated, a client
has unrestricted network access through the tunnel. This is a security gap: a
compromised key grants unrestricted access.
Operators need the ability to:
- Restrict which hosts and ports authenticated clients can access
- Apply different rules to different principals (key fingerprints, accounts)
- Restrict WebTransport clients to alknet control channels only
- Set a default policy (allow-all for migration compatibility, deny-all for production)
Decision
Add ForwardingPolicy as part of DynamicConfig (reloadable without
restart).
Type Definitions
pub struct ForwardingPolicy {
pub default: ForwardingAction,
pub rules: Vec<ForwardingRule>,
}
pub struct ForwardingRule {
pub target: TargetPattern,
pub action: ForwardingAction,
pub principals: Vec<String>, // Empty = matches all
pub transports: Vec<TransportKind>, // Empty = matches all
}
pub enum ForwardingAction {
Allow,
Deny,
}
pub enum TargetPattern {
Any,
Host(String), // "localhost", "*.example.com"
Cidr(IpNetwork), // "10.0.0.0/8"
PortRange(String, Range<u16>), // "localhost", ports 8080-8090
AlknetPrefix, // Matches alknet-* control channels
}
Rule Evaluation
Rules are evaluated in order. First match wins. If no rule matches, the default applies. This supports both allowlist and blocklist semantics:
- Allowlist:
default: Deny, then explicit Allow rules for permitted destinations. - Blocklist:
default: Allow, then explicit Deny rules for blocked destinations.
Principals
Each rule can specify which principals it applies to. A principal is an
Identity.id (fingerprint or UUID) or a scope from Identity.scopes. When the
rule's principals field is empty, it matches all identities.
This connects to the IdentityProvider trait (ADR-029): when a client
authenticates, the Identity is resolved, and the forwarding policy checks
rules against Identity.id and Identity.scopes.
TransportKind-Aware Rules
Each rule can specify which TransportKind it applies to. This enables
transport-specific restrictions — for example, WebTransport clients can be
restricted to alknet-* control channels only:
ForwardingRule {
target: TargetPattern::AlknetPrefix,
action: ForwardingAction::Allow,
principals: vec![],
transports: vec![TransportKind::WebTransport { host: "*".into() }],
}
Where the Policy Check Happens
The forwarding policy check occurs in channel_open_direct_tcpip before the
proxy task is spawned. The current behavior (no check) is equivalent to
ForwardingPolicy::allow_all() — default Allow, no rules. This preserves
backward compatibility during migration.
DynamicConfig Integration
ForwardingPolicy is part of DynamicConfig and reloadable via
ConfigReloadHandle::reload() or NAPI's reloadForwarding(). Changes take
effect on the next channel open — existing connections continue with their
current policy.
OQ Resolutions
- OQ-12 (Per-user forwarding scope vs global rules): Resolved. Start with
global rules + principal matching from
Identity.scopes. Per-user scope frompeer_credentials.metadata.scopesviaIdentityProvider. - OQ-16 (Transport-specific forwarding): Resolved. Add
TransportKindmatch inForwardingRule. WebTransport clients can be restricted. - OQ-18 (Source of Identity.scopes): Resolved by ADR-029 and this ADR.
IdentityProviderowns scopes.ForwardingPolicyconsumes them.
Consequences
- Positive: Operators can restrict access per identity, per destination, per transport. A compromised key no longer grants unrestricted network access.
- Positive: Default-allow preserves current behavior during migration. Switch to default-deny for production deployments.
- Positive: Policy is reloadable without restart. Adding a rule via
reloadForwarding()takes effect on the next channel open. - Positive:
TransportKind-aware rules enable transport-specific restrictions (e.g., WebTransport clients restricted to alknet-* channels). - Negative: Another check in the hot path (every
channel_open_direct_tcpipcall). The cost is a linear scan of rules — acceptable for small rule sets. Large rule sets should use compiled matchers (future optimization). - Negative:
TargetPatternstring matching is lenient. Host patterns like*.example.comrequire careful implementation to prevent bypasses. Thegloborglobsetcrate can handle this correctly.
References
- research/configuration.md — ForwardingPolicy section
- auth.md — Identity.scopes and IdentityProvider
- open-questions.md — OQ-12, OQ-16, OQ-18
- ADR-029 — Identity as core type
- ADR-030 — DynamicConfig (ForwardingPolicy is part of it)
- integration-plan.md — Phase 1.3