Skip to content

feat: QUIC agent tunnel — protocol, listener, agent client#1738

Draft
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 1 commit intomasterfrom
feat/quic-tunnel-1-core
Draft

feat: QUIC agent tunnel — protocol, listener, agent client#1738
irvingouj@Devolutions (irvingoujAtDevolution) wants to merge 1 commit intomasterfrom
feat/quic-tunnel-1-core

Conversation

@irvingoujAtDevolution
Copy link
Copy Markdown
Contributor

@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) commented Apr 2, 2026

Summary

Add QUIC-based agent tunnel core infrastructure (PR 1 of 4).

Agents in private networks connect outbound to Gateway via QUIC/mTLS, advertise reachable subnets and domains, and proxy TCP connections on behalf of Gateway. No inbound firewall rules needed.

See the Technical Specification comment for detailed protocol design (enrollment, multiplexing, user flow).

PR stack

  1. Protocol + Tunnel Core (this PR)
  2. Transparent Routing — SSH/RDP/KDC through tunnel
  3. Auth + Webapp — scope token exchange, management UI
  4. Deployment + Installer — Docker, MSI

What's in this PR

  • crates/agent-tunnel-proto/ — protocol types (RouteAdvertise, ConnectMessage, Heartbeat)
  • devolutions-gateway/src/agent_tunnel/ — QUIC listener, agent registry, CA/cert management, stream abstraction
  • devolutions-agent/src/tunnel.rs — QUIC client with auto-reconnect (exponential backoff)
  • devolutions-agent/src/enrollment.rs — CSR-based enrollment (private key never leaves agent)
  • devolutions-agent/src/domain_detect.rs — AD domain auto-detection (Windows/Linux)
  • devolutions-gateway/src/api/agent_enrollment.rs — enrollment + management API endpoints

Security highlights

  • CSR-based enrollment — agent generates key pair locally, only CSR transmitted
  • mTLS with private PKI — no system trust store dependency
  • Bounded bincode deserialization — prevents OOM via crafted payloads
  • Buffer size limits on all streams — control (1 MiB), session (64 KiB)
  • Bounded read channels with backpressure
  • Agent subnet validation — prevents use as open proxy
  • Constant-time secret comparison (SHA-256 digest)
  • Connection limit (max 1000 agents)

Test plan

  • 36 unit/integration tests pass
  • 15 proto crate tests pass
  • cargo clippy --workspace --tests -- -D warnings clean
  • cargo +nightly fmt --all -- --check clean
  • Manual: agent enrolls and connects via QUIC
  • Manual: agent auto-reconnects after gateway restart

@irvingoujAtDevolution
Copy link
Copy Markdown
Contributor Author

QUIC Agent Tunnel — Technical Specification

1. Enrollment

How an agent gets its certificate

Admin                        Agent Machine                  Gateway
  │                              │                             │
  │  Click "Enroll Agent"        │                             │
  │  in DVLS / Gateway webapp    │                             │
  │                              │                             │
  │  Copy enrollment string      │                             │
  │  dgw-enroll:v1:base64...    │                             │
  │                              │                             │
  │  Paste into MSI installer    │                             │
  │  or CLI command              │                             │
  │                              │                             │
  │                              │  1. Decode enrollment string │
  │                              │     → gateway URL            │
  │                              │     → one-time token         │
  │                              │     → QUIC endpoint          │
  │                              │                             │
  │                              │  2. Generate ECDSA P-256     │
  │                              │     key pair LOCALLY         │
  │                              │     Write key to disk (0600) │
  │                              │                             │
  │                              │  3. Generate CSR             │
  │                              │     (public key + signature) │
  │                              │                             │
  │                              │── POST /enroll ────────────>│
  │                              │   { agent_name, csr_pem }   │
  │                              │   Bearer: <one-time-token>  │
  │                              │                             │
  │                              │                             │  4. Validate token
  │                              │                             │     (consumed, cannot replay)
  │                              │                             │  5. Verify CSR signature
  │                              │                             │  6. Assign agent UUID
  │                              │                             │  7. Sign cert with CA
  │                              │                             │     (embed UUID in SAN)
  │                              │                             │
  │                              │<────────────────────────────│
  │                              │   { agent_id, cert_pem,     │
  │                              │     ca_cert_pem, endpoint } │
  │                              │                             │
  │                              │  8. Write cert + CA cert     │
  │                              │  9. Update agent.json        │
  │                              │     (Tunnel section only,    │
  │                              │      preserves other config) │
  │                              │                             │
  │                              │  10. Connect via QUIC ──────>│
  │                              │      (mTLS with new cert)    │

Key property: the private key never leaves the agent machine.
Only the CSR (containing the public key and a proof-of-possession signature) is transmitted.
The enrollment response contains only the signed certificate and the CA certificate — no secrets.

Enrollment token

The enrollment token is either:

  • A one-time UUID (122-bit entropy) generated by the gateway — consumed atomically on use, cannot be replayed.
  • A static secret from gateway configuration — compared in constant time.

2. Stream Multiplexing

One QUIC connection, many independent streams

Agent ←──── single QUIC connection ────→ Gateway
             │
             ├── Stream 0 (control, always open)
             │   Agent → GW:  RouteAdvertise every 30s
             │   Agent → GW:  Heartbeat every 60s
             │   GW → Agent:  HeartbeatAck
             │
             ├── Stream 1 (RDP session #1)
             │   GW → Agent:  ConnectMessage { target: 10.0.0.5:3389 }
             │   Agent → GW:  ConnectResponse::Success
             │   Then: raw bidirectional bytes (RDP protocol data)
             │
             ├── Stream 5 (SSH session #1)
             │   GW → Agent:  ConnectMessage { target: 10.0.0.10:22 }
             │   Agent → GW:  ConnectResponse::Success
             │   Then: raw bidirectional bytes (SSH protocol data)
             │
             └── Stream 9 (SSH session #2)
                 GW → Agent:  ConnectMessage { target: 10.0.0.20:22 }
                 Agent → GW:  ConnectResponse::Success
                 Then: raw bidirectional bytes (SSH protocol data)

Each stream is independently ordered.
A retransmission on stream 1 does not block streams 5 or 9.
This is QUIC's core advantage over TCP — no head-of-line blocking across streams.

How a new session is established

  1. Gateway allocates the next server-initiated stream ID (1, 5, 9, 13, …).
  2. Gateway writes a length-prefixed ConnectMessage to the new stream.
  3. Agent reads the stream, decodes the ConnectMessage.
  4. Agent validates the target IP is within its advertised subnets (security boundary).
  5. Agent opens a TCP connection to the target.
  6. Agent writes ConnectResponse::Success back on the same stream.
  7. From this point, every byte on the QUIC stream is forwarded 1:1 to/from the TCP connection.

No new QUIC handshake is needed — streams are opened instantly on the existing connection.

Message encoding

All control and session setup messages use length-prefixed bincode:

┌─────────────────────────┬──────────────────────────────┐
│ 4 bytes (big-endian u32)│ N bytes (bincode payload)    │
│ message_length = N      │                              │
└─────────────────────────┴──────────────────────────────┘

After ConnectResponse::Success, the stream carries raw bytes — no framing, no headers.
The gateway and agent act as transparent TCP proxies.

Size limits

Message type Max size Purpose
Control messages 1 MiB RouteAdvertise, Heartbeat
Session messages 64 KiB ConnectMessage, ConnectResponse

Limits are enforced on the length prefix (before reading the payload) and on the bincode deserializer (prevents crafted payloads with huge internal Vec lengths).

3. User Experience

Network topology

┌─────────────────────────────────────────────────────────┐
│  Cloud                                                   │
│  ┌──────────────────┐                                   │
│  │ Devolutions      │                                   │
│  │ Gateway          │  ← publicly reachable              │
│  │ gateway.acme.com │                                   │
│  └────────┬─────────┘                                   │
│           │ QUIC (UDP 4433)                              │
└───────────┼─────────────────────────────────────────────┘
            │
       ─ ─ ─│─ ─ ─ ─ ─ ─ firewall (outbound only) ─ ─ ─ ─
            │
┌───────────┼─────────────────────────────────────────────┐
│  Office   │                                              │
│  ┌────────┴─────────┐    ┌──────────┐  ┌──────────┐    │
│  │ Agent            │    │ DC       │  │ File     │    │
│  │ 10.10.0.8        │───→│ 10.10.0.3│  │ Server   │    │
│  │ advertises:      │    │ (RDP+KDC)│  │ 10.10.0.5│    │
│  │  10.10.0.0/24    │    └──────────┘  └──────────┘    │
│  │  contoso.local   │                                   │
│  └──────────────────┘                                   │
└─────────────────────────────────────────────────────────┘

Admin setup (one-time)

  1. Open Gateway webapp → Agents → Enroll Agent.
  2. Copy the enrollment string.
  3. On the agent machine: devolutions-agent up --enrollment-string "dgw-enroll:v1:...".
  4. Agent enrolls, connects, starts advertising 10.10.0.0/24 + contoso.local.

End-user workflow (daily use)

The user has no awareness of the agent. From their perspective:

  1. Open RDM or Gateway webapp.
  2. Create an RDP connection to 10.10.0.3.
  3. Click connect.
  4. The RDP desktop appears.

What happens behind the scenes:

User's browser
  → WebSocket to Gateway (gateway.acme.com)
    → Gateway routing: 10.10.0.3 matches agent's 10.10.0.0/24 subnet
      → Gateway opens QUIC stream 5 to agent
        → ConnectMessage { target: "10.10.0.3:3389" }
          → Agent connects TCP to 10.10.0.3:3389
            → ConnectResponse::Success
              → RDP data flows bidirectionally

No VPN. No inbound firewall rules on the office network. No routing configuration.

Transparent routing rules

When a connection request arrives, the gateway evaluates routing in priority order:

  1. Explicit agent ID — if the session token contains jet_agent_id, route to that specific agent.
  2. IP subnet match — if the target is an IP address, find agents whose advertised subnets contain it.
  3. Domain suffix match — if the target is a hostname, find agents whose advertised domains match by longest suffix (e.g., db01.finance.contoso.local matches finance.contoso.local over contoso.local).
  4. No match — direct connection (gateway connects to the target itself, no tunnel).

When multiple agents match the same target, the most recently seen agent is tried first.
If it fails, the next candidate is tried (automatic failover).

Resilience

  • Agent auto-reconnects if the QUIC connection drops (exponential backoff, 1s–60s, with jitter).
  • Config re-read on every reconnection attempt (admin can change subnets without restarting the service).
  • Heartbeat monitoring — agents are marked offline after 90 seconds without a heartbeat.
  • Graceful shutdown — agent sends QUIC close frame, gateway immediately unregisters it from routing.

1 similar comment
@irvingoujAtDevolution
Copy link
Copy Markdown
Contributor Author

QUIC Agent Tunnel — Technical Specification

1. Enrollment

How an agent gets its certificate

Admin                        Agent Machine                  Gateway
  │                              │                             │
  │  Click "Enroll Agent"        │                             │
  │  in DVLS / Gateway webapp    │                             │
  │                              │                             │
  │  Copy enrollment string      │                             │
  │  dgw-enroll:v1:base64...    │                             │
  │                              │                             │
  │  Paste into MSI installer    │                             │
  │  or CLI command              │                             │
  │                              │                             │
  │                              │  1. Decode enrollment string │
  │                              │     → gateway URL            │
  │                              │     → one-time token         │
  │                              │     → QUIC endpoint          │
  │                              │                             │
  │                              │  2. Generate ECDSA P-256     │
  │                              │     key pair LOCALLY         │
  │                              │     Write key to disk (0600) │
  │                              │                             │
  │                              │  3. Generate CSR             │
  │                              │     (public key + signature) │
  │                              │                             │
  │                              │── POST /enroll ────────────>│
  │                              │   { agent_name, csr_pem }   │
  │                              │   Bearer: <one-time-token>  │
  │                              │                             │
  │                              │                             │  4. Validate token
  │                              │                             │     (consumed, cannot replay)
  │                              │                             │  5. Verify CSR signature
  │                              │                             │  6. Assign agent UUID
  │                              │                             │  7. Sign cert with CA
  │                              │                             │     (embed UUID in SAN)
  │                              │                             │
  │                              │<────────────────────────────│
  │                              │   { agent_id, cert_pem,     │
  │                              │     ca_cert_pem, endpoint } │
  │                              │                             │
  │                              │  8. Write cert + CA cert     │
  │                              │  9. Update agent.json        │
  │                              │     (Tunnel section only,    │
  │                              │      preserves other config) │
  │                              │                             │
  │                              │  10. Connect via QUIC ──────>│
  │                              │      (mTLS with new cert)    │

Key property: the private key never leaves the agent machine.
Only the CSR (containing the public key and a proof-of-possession signature) is transmitted.
The enrollment response contains only the signed certificate and the CA certificate — no secrets.

Enrollment token

The enrollment token is either:

  • A one-time UUID (122-bit entropy) generated by the gateway — consumed atomically on use, cannot be replayed.
  • A static secret from gateway configuration — compared in constant time.

2. Stream Multiplexing

One QUIC connection, many independent streams

Agent ←──── single QUIC connection ────→ Gateway
             │
             ├── Stream 0 (control, always open)
             │   Agent → GW:  RouteAdvertise every 30s
             │   Agent → GW:  Heartbeat every 60s
             │   GW → Agent:  HeartbeatAck
             │
             ├── Stream 1 (RDP session #1)
             │   GW → Agent:  ConnectMessage { target: 10.0.0.5:3389 }
             │   Agent → GW:  ConnectResponse::Success
             │   Then: raw bidirectional bytes (RDP protocol data)
             │
             ├── Stream 5 (SSH session #1)
             │   GW → Agent:  ConnectMessage { target: 10.0.0.10:22 }
             │   Agent → GW:  ConnectResponse::Success
             │   Then: raw bidirectional bytes (SSH protocol data)
             │
             └── Stream 9 (SSH session #2)
                 GW → Agent:  ConnectMessage { target: 10.0.0.20:22 }
                 Agent → GW:  ConnectResponse::Success
                 Then: raw bidirectional bytes (SSH protocol data)

Each stream is independently ordered.
A retransmission on stream 1 does not block streams 5 or 9.
This is QUIC's core advantage over TCP — no head-of-line blocking across streams.

How a new session is established

  1. Gateway allocates the next server-initiated stream ID (1, 5, 9, 13, …).
  2. Gateway writes a length-prefixed ConnectMessage to the new stream.
  3. Agent reads the stream, decodes the ConnectMessage.
  4. Agent validates the target IP is within its advertised subnets (security boundary).
  5. Agent opens a TCP connection to the target.
  6. Agent writes ConnectResponse::Success back on the same stream.
  7. From this point, every byte on the QUIC stream is forwarded 1:1 to/from the TCP connection.

No new QUIC handshake is needed — streams are opened instantly on the existing connection.

Message encoding

All control and session setup messages use length-prefixed bincode:

┌─────────────────────────┬──────────────────────────────┐
│ 4 bytes (big-endian u32)│ N bytes (bincode payload)    │
│ message_length = N      │                              │
└─────────────────────────┴──────────────────────────────┘

After ConnectResponse::Success, the stream carries raw bytes — no framing, no headers.
The gateway and agent act as transparent TCP proxies.

Size limits

Message type Max size Purpose
Control messages 1 MiB RouteAdvertise, Heartbeat
Session messages 64 KiB ConnectMessage, ConnectResponse

Limits are enforced on the length prefix (before reading the payload) and on the bincode deserializer (prevents crafted payloads with huge internal Vec lengths).

3. User Experience

Network topology

┌─────────────────────────────────────────────────────────┐
│  Cloud                                                   │
│  ┌──────────────────┐                                   │
│  │ Devolutions      │                                   │
│  │ Gateway          │  ← publicly reachable              │
│  │ gateway.acme.com │                                   │
│  └────────┬─────────┘                                   │
│           │ QUIC (UDP 4433)                              │
└───────────┼─────────────────────────────────────────────┘
            │
       ─ ─ ─│─ ─ ─ ─ ─ ─ firewall (outbound only) ─ ─ ─ ─
            │
┌───────────┼─────────────────────────────────────────────┐
│  Office   │                                              │
│  ┌────────┴─────────┐    ┌──────────┐  ┌──────────┐    │
│  │ Agent            │    │ DC       │  │ File     │    │
│  │ 10.10.0.8        │───→│ 10.10.0.3│  │ Server   │    │
│  │ advertises:      │    │ (RDP+KDC)│  │ 10.10.0.5│    │
│  │  10.10.0.0/24    │    └──────────┘  └──────────┘    │
│  │  contoso.local   │                                   │
│  └──────────────────┘                                   │
└─────────────────────────────────────────────────────────┘

Admin setup (one-time)

  1. Open Gateway webapp → Agents → Enroll Agent.
  2. Copy the enrollment string.
  3. On the agent machine: devolutions-agent up --enrollment-string "dgw-enroll:v1:...".
  4. Agent enrolls, connects, starts advertising 10.10.0.0/24 + contoso.local.

End-user workflow (daily use)

The user has no awareness of the agent. From their perspective:

  1. Open RDM or Gateway webapp.
  2. Create an RDP connection to 10.10.0.3.
  3. Click connect.
  4. The RDP desktop appears.

What happens behind the scenes:

User's browser
  → WebSocket to Gateway (gateway.acme.com)
    → Gateway routing: 10.10.0.3 matches agent's 10.10.0.0/24 subnet
      → Gateway opens QUIC stream 5 to agent
        → ConnectMessage { target: "10.10.0.3:3389" }
          → Agent connects TCP to 10.10.0.3:3389
            → ConnectResponse::Success
              → RDP data flows bidirectionally

No VPN. No inbound firewall rules on the office network. No routing configuration.

Transparent routing rules

When a connection request arrives, the gateway evaluates routing in priority order:

  1. Explicit agent ID — if the session token contains jet_agent_id, route to that specific agent.
  2. IP subnet match — if the target is an IP address, find agents whose advertised subnets contain it.
  3. Domain suffix match — if the target is a hostname, find agents whose advertised domains match by longest suffix (e.g., db01.finance.contoso.local matches finance.contoso.local over contoso.local).
  4. No match — direct connection (gateway connects to the target itself, no tunnel).

When multiple agents match the same target, the most recently seen agent is tried first.
If it fails, the next candidate is tried (automatic failover).

Resilience

  • Agent auto-reconnects if the QUIC connection drops (exponential backoff, 1s–60s, with jitter).
  • Config re-read on every reconnection attempt (admin can change subnets without restarting the service).
  • Heartbeat monitoring — agents are marked offline after 90 seconds without a heartbeat.
  • Graceful shutdown — agent sends QUIC close frame, gateway immediately unregisters it from routing.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the first slice of a QUIC/mTLS “agent tunnel” system: a shared binary protocol crate, a Gateway-side QUIC listener/registry/enrollment API, and an Agent-side enrollment + reconnecting tunnel client. This enables routing Gateway-initiated TCP proxy sessions through outbound-connected agents (for private-network reachability).

Changes:

  • Introduces agent-tunnel-proto crate (control/session messages, framing, protocol versioning).
  • Adds Gateway agent-tunnel core (agent_tunnel module), config wiring, REST endpoints, and token claim support (jet_agent_id) used in the forwarding path.
  • Adds Agent enrollment/bootstrap + QUIC tunnel client with auto-reconnect and domain auto-detection.

Reviewed changes

Copilot reviewed 35 out of 36 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
devolutions-gateway/tests/config.rs Updates config samples to include agent_tunnel field.
devolutions-gateway/src/token.rs Adds jet_agent_id to association claims; adjusts scope token claims serialization/visibility.
devolutions-gateway/src/service.rs Initializes and registers the agent-tunnel listener task when enabled.
devolutions-gateway/src/ngrok.rs Threads agent_tunnel_handle into the TCP tunnel client path.
devolutions-gateway/src/middleware/auth.rs Adds auth exception for /jet/agent-tunnel/enroll (self-auth via bearer token).
devolutions-gateway/src/listener.rs Threads agent_tunnel_handle into the generic client path.
devolutions-gateway/src/lib.rs Exposes agent_tunnel module and adds agent_tunnel_handle to DgwState.
devolutions-gateway/src/generic_client.rs Uses jet_agent_id to route Fwd connections through the agent tunnel.
devolutions-gateway/src/extract.rs Adds request extractors for agent-management read/write access control.
devolutions-gateway/src/config.rs Adds AgentTunnelConf to Gateway config DTO and runtime config.
devolutions-gateway/src/api/webapp.rs Ensures new jet_agent_id claim is present (set to None) when minting tokens.
devolutions-gateway/src/api/mod.rs Nests the new /jet/agent-tunnel/* router.
devolutions-gateway/src/api/agent_enrollment.rs Implements enrollment + agent management endpoints (list/get/delete/resolve-target).
devolutions-gateway/src/agent_tunnel/mod.rs Declares agent-tunnel submodules and re-exports core types.
devolutions-gateway/src/agent_tunnel/listener.rs QUIC UDP listener event loop + proxy-stream request dispatching.
devolutions-gateway/src/agent_tunnel/enrollment_store.rs In-memory single-use enrollment token store with expiry.
devolutions-gateway/src/agent_tunnel/stream.rs Tokio AsyncRead/AsyncWrite wrapper over QUIC streams via channels.
devolutions-gateway/src/agent_tunnel/registry.rs Agent registry with heartbeat liveness + subnet/domain routing selection.
devolutions-gateway/src/agent_tunnel/connection.rs Managed quiche connection: handshake identity, control parsing, proxy stream setup.
devolutions-gateway/src/agent_tunnel/cert.rs CA manager for enrollment signing + server cert issuance and cert parsing helpers.
devolutions-gateway/Cargo.toml Adds QUIC/proto/cert/routing dependencies for the tunnel feature.
devolutions-agent/src/service.rs Registers TunnelTask when tunnel is enabled; fixes conf_handle cloning for RDP task.
devolutions-agent/src/main.rs Adds CLI support for enroll/up bootstrap flows and parsing helpers + tests.
devolutions-agent/src/lib.rs Exposes new modules: tunnel, enrollment, domain_detect.
devolutions-agent/src/enrollment.rs Implements enrollment request + persistence of certs/config merge.
devolutions-agent/src/domain_detect.rs Adds Windows/Linux DNS domain auto-detection helper.
devolutions-agent/src/tunnel.rs Implements reconnecting QUIC client + control/session stream handling and TCP proxying.
devolutions-agent/src/config.rs Adds tunnel config section; makes save_config/get_conf_file_path public.
devolutions-agent/Cargo.toml Adds proto/quiche/reqwest/rcgen dependencies and Windows feature for domain detection.
crates/agent-tunnel-proto/src/lib.rs Defines the protocol crate API surface and exports.
crates/agent-tunnel-proto/src/version.rs Adds protocol version constants + validation helper.
crates/agent-tunnel-proto/src/error.rs Defines protocol-level error types.
crates/agent-tunnel-proto/src/control.rs Adds control-plane message definitions + framed encode/decode.
crates/agent-tunnel-proto/src/session.rs Adds session-plane message definitions + framed encode/decode.
crates/agent-tunnel-proto/Cargo.toml New crate manifest and dependencies.
Cargo.lock Locks new dependencies introduced for QUIC, cert handling, registry, and protocol crate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@irvingoujAtDevolution irvingouj@Devolutions (irvingoujAtDevolution) force-pushed the feat/quic-tunnel-1-core branch 8 times, most recently from ec35e22 to 9e345b7 Compare April 6, 2026 21:58
Add QUIC-based agent tunnel core infrastructure. Agents in private
networks connect outbound to Gateway via QUIC/mTLS, advertise reachable
subnets and domains, and proxy TCP connections on behalf of Gateway.

Protocol (agent-tunnel-proto crate):
- RouteAdvertise with subnets + domain advertisements
- ConnectMessage/ConnectResponse for session stream setup
- Heartbeat/HeartbeatAck for liveness detection
- Protocol version negotiation (v2)

Gateway (agent_tunnel module):
- QUIC listener with mTLS authentication
- Agent registry with subnet/domain tracking
- Certificate authority for agent enrollment
- Enrollment token store (one-time tokens)
- Bidirectional proxy stream multiplexing

Agent (devolutions-agent):
- QUIC client with auto-reconnect and exponential backoff
- Agent enrollment with config merge (preserves existing settings)
- Domain auto-detection (Windows: USERDNSDOMAIN, Linux: resolv.conf)
- Subnet validation on incoming connections
- Certificate file permissions (0o600 on Unix)

API endpoints:
- POST /jet/agent-tunnel/enroll — agent enrollment
- GET /jet/agent-tunnel/agents — list agents
- GET /jet/agent-tunnel/agents/{id} — get agent
- DELETE /jet/agent-tunnel/agents/{id} — delete agent
- POST /jet/agent-tunnel/agents/resolve-target — routing diagnostics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants