Skip to main content

aws-ssm-bridge

A Rust implementation of the AWS Systems Manager Session Manager protocol, with async Python bindings.

License Rust Python unsafe

Not affiliated with AWS. This is an independent implementation of a documented-by-observation protocol, not endorsed or sponsored by Amazon Web Services, Inc.


What this is for

The official session-manager-plugin is a CLI binary: you shell out to it, hand it JSON on argv, and parse whatever it prints. That is fine for a terminal and awkward for everything else.

aws-ssm-bridge is a library. Open sessions, stream bytes and forward ports from inside your own async application — no subprocess, no plugin to install, no output scraping.

use aws_ssm_bridge::SessionBuilder;
use futures_util::StreamExt;

let session = SessionBuilder::new("i-0123456789abcdef0").start().await?;
session.wait_ready().await?;

let mut output = session.output();
session.send(&b"uname -a\r"[..]).await?;

while let Some(chunk) = output.next().await {
    print!("{}", String::from_utf8_lossy(&chunk));
}
session.terminate().await?;

Install

cargo add aws-ssm-bridge
cargo add tokio --features rt-multi-thread,macros
pip install aws-ssm-bridge

Requires the same IAM permissions as the official plugin: ssm:StartSession on the target, and ssm:TerminateSession on your own sessions.


Capabilities

Shell and command sessions Interactive shells, AWS-StartInteractiveCommand, AWS-StartNonInteractiveCommand
Port forwarding smux-multiplexed, many concurrent TCP connections over one session
KMS session encryption AES-256-GCM end-to-end, for accounts that mandate encrypted sessions
Interactive terminal Raw byte passthrough, SIGWINCH resize, panic-safe restore
Reconnection Durable output stream across reconnects, full-jitter backoff
Pooling Bounded concurrent sessions with automatic reaping
Observability tracing spans throughout, pluggable metrics recorder
Python Full async API, type stubs, context managers

Verified against a live SSM agent (3.3.3572.0): the SigV4-signed data-channel upgrade, shell sessions, the handshake, concurrent multiplexed TCP streams, and clean teardown.


Guided tour

Shell session

use aws_ssm_bridge::SessionBuilder;
use futures_util::StreamExt;

let session = SessionBuilder::new("i-0123456789abcdef0")
    .region("eu-central-1")
    .reason("incident 4711")     // recorded in CloudTrail
    .start()
    .await?;

let mut output = session.output();   // subscribe *before* sending
session.wait_ready().await?;
session.send(&b"df -h\r"[..]).await?;

Send \r, not \n: a remote pty maps carriage return to newline, but Windows shells behind winpty do not accept a bare line feed.

Port forwarding

use std::sync::Arc;
use aws_ssm_bridge::{
    documents::PortForwardingToRemoteHost, install_signal_handlers,
    PortForwardConfig, PortForwarder, SessionBuilder, ShutdownSignal,
};

let shutdown = ShutdownSignal::new();
install_signal_handlers(shutdown.clone());

let session = Arc::new(
    SessionBuilder::new("i-0123456789abcdef0")
        .document(PortForwardingToRemoteHost::new("db.internal", 5432))
        .start()
        .await?,
);

let forwarder = PortForwarder::bind(PortForwardConfig {
    local_addr: "127.0.0.1:15432".parse()?,
    ..Default::default()
})
.await?;

println!("psql -h 127.0.0.1 -p {}", forwarder.local_addr().port());
forwarder.forward(session, shutdown).await?;

Each accepted connection becomes its own smux stream inside one WebSocket, so concurrent connections neither block nor corrupt each other.

Typed documents

use aws_ssm_bridge::documents::*;

PortForwardingSession::new(3306)                            // port on the instance
PortForwardingToRemoteHost::new("db.internal", 5432)        // through the instance
SshSession::new()                                           // ssh ProxyCommand transport
InteractiveCommand::new("top")                              // with a pty
NonInteractiveCommand::new("systemctl status nginx")        // without a pty

Python

import asyncio
from aws_ssm_bridge import SessionManager

async def main():
    manager = await SessionManager.new(region="eu-central-1")
    async with await manager.start_session("i-0123456789abcdef0") as session:
        await session.send(b"uname -a\r")
        async for chunk in session.output():
            print(chunk.decode(errors="replace"), end="")

asyncio.run(main())

Session lifetime

A session is either running or closed. Every way it can end — a clean terminate(), the agent hanging up, a dead network, a protocol violation — resolves Session::closed() and records a CloseReason.

tokio::select! {
    () = session.closed() => eprintln!("gone: {}", session.close_reason().unwrap()),
    result = do_work(&session) => result?,
}

That one guarantee is what makes the layers above it work: the port forwarder stops accepting when the tunnel dies, the pool reaps dead entries, and ReconnectingSession knows when to rebuild. There is no state in which the handle looks alive but nothing is running.

Reconnection resumes the session where it can: ssm:ResumeSession keeps the same session and the same remote process, so the shell survives the gap. When the session has timed out rather than merely disconnected, it cannot be resumed and a new one is started instead — a new process, so shell state and anything printed while disconnected are gone. ReconnectEvent tells the two apart: Resumed carries the same session ID, Reconnected a new one.


Feature flags

Feature Default Effect
interactive ✅ terminal and InteractiveShell; pulls in crossterm
kms ✅ KMS session encryption; pulls in aws-sdk-kms and aes-gcm
python — PyO3 bindings
extension-module — Link the bindings as a Python extension module; set by maturin when building a wheel

Built without kms, a session whose account mandates encryption fails the handshake with an explicit error instead of quietly running in plaintext.

extension-module is deliberately separate from python: it leaves the CPython symbols for the interpreter to resolve at load time, which is right for a wheel and fatal for a test binary. Because --all-features would enable it, name the features you want instead — --features interactive,kms is what CI runs.


Examples

Rust
cargo run --example shell -- i-… "uname -a" Run a command, print the output
cargo run --example interactive -- i-… Full interactive shell
cargo run --example port_forward -- i-… 5432 127.0.0.1:15432 TCP tunnel
cargo run --example reconnecting -- i-… Survive a dropped connection
cargo run --example fleet -- "uptime" i-… i-… One command, many instances
cargo run --example metrics -- i-… Wire up the metrics hooks

Python equivalents live in python_examples/.


Documentation

Full documentation: hupe1980.github.io/aws-ssm-bridge

Getting started Credentials, your first session, port forwarding, every CloseReason
Architecture How it is layered, and why the non-obvious parts are that way
Wire protocol The binary format, reliability, smux, KMS encryption
Security Threat model, and what is explicitly not defended against
Python API The full async binding surface
API reference Every type and method, on docs.rs
Changelog What changed, and how to migrate

The site is built with Zola from site/; just site serves it locally.


Security

  • unsafe_code = "forbid" for the whole crate, so any unsafe fails the build.
  • The session token travels only in the data-channel open message — never in a URL, where proxies and traces would record it.
  • The data-channel WebSocket upgrade is signed with SigV4 over ssmmessages, matching the reference plugin. A presigned stream URL, and a session opened with Session::attach, are sent unsigned.
  • SHA-256 payload digests are verified, matching the reference implementation; a mismatch ends the session rather than delivering corrupt bytes.
  • The data channel refuses any endpoint that is not an AWS SSM messages host.
  • KMS session encryption is AES-256-GCM with a per-message nonce, and a client that cannot negotiate it fails the handshake rather than downgrading.

See the security model for the threat model and what is explicitly not defended against.


Development

just                # list every recipe
just check          # what CI runs: fmt, clippy, tests, docs
just matrix         # every feature combination
just bench          # framing and reliability micro-benchmarks
just fuzz           # cargo-fuzz over the network-facing parsers
just python         # build and install the wheel
just site           # serve the documentation site locally
just release-check  # everything above, plus MSRV, --locked, publish dry run
SSM_TARGET=i-… just live-smoke   # the pre-release check against a real instance

The MSRV is set by the AWS SDK, not by this crate's own code. just msrv verifies it against the committed lockfile — which is the only way the check means anything, since an unlocked resolve pulls in dependencies that need a newer toolchain than users actually get.


License

MIT. See LICENSE.

Metadata

Release files for aws-ssm-bridge 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aws-ssm-bridge 0.6.0
File Size Uploaded
aws_ssm_bridge-0.6.0.tar.gz 354.4 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for aws-ssm-bridge 0.6.0
File Interpreter ABI Platform
aws_ssm_bridge-0.6.0-cp38-abi3-win_amd64.whl CPython 3.8 abi3 Windows x86-64 Details
aws_ssm_bridge-0.6.0-cp38-abi3-manylinux_2_39_x86_64.whl CPython 3.8 abi3 Linux glibc 2.39+ x86-64 Details
aws_ssm_bridge-0.6.0-cp38-abi3-macosx_11_0_arm64.whl CPython 3.8 abi3 macOS 11.0+ ARM64 Details

Total release size: 18.3 MB

Release files / aws_ssm_bridge-0.6.0.tar.gz

Download URL aws_ssm_bridge-0.6.0.tar.gz
Size 354.4 kB
Tags Source
SHA-256 checksum
How to use checksums
67edcdde52738942ab60b115c616c95b9f6cfb3ef7cfe55becd87d4320290a1f
BLAKE2b-256 checksum
How to use checksums
5c7dae8fb2d4a48ff3a62ea644de488dacaaf5d140ab8a4c00a0b1e3c821a139
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / aws_ssm_bridge-0.6.0-cp38-abi3-win_amd64.whl

Download URL aws_ssm_bridge-0.6.0-cp38-abi3-win_amd64.whl
Size 5.2 MB
Tags CPython 3.8 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
149bfa869ea5ffd69bebe2ccecd9a9d3e49014480481b8a1ab1a690058346bcb
BLAKE2b-256 checksum
How to use checksums
2e45bf8f5505e5a40a6f96fdcd30d6e77ce296ea0e82c7f7b6126bae98b039c5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / aws_ssm_bridge-0.6.0-cp38-abi3-manylinux_2_39_x86_64.whl

Download URL aws_ssm_bridge-0.6.0-cp38-abi3-manylinux_2_39_x86_64.whl
Size 7.8 MB
Tags CPython 3.8 Linux glibc 2.39+ x86-64 abi3
SHA-256 checksum
How to use checksums
191aa1135579d9e424a9582f7b795c6ab9d58c31a0b977ccbd69df340dc5ab16
BLAKE2b-256 checksum
How to use checksums
f10aa7d06501a4b3361ffd9fdca1aa010dd8e662053d7265173f01504578bd70
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / aws_ssm_bridge-0.6.0-cp38-abi3-macosx_11_0_arm64.whl

Download URL aws_ssm_bridge-0.6.0-cp38-abi3-macosx_11_0_arm64.whl
Size 5.0 MB
Tags CPython 3.8 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
bd229b66b1c7afd3f4ecd5cca021845bc64d042b985f629f02534ca3d9d39bc8
BLAKE2b-256 checksum
How to use checksums
1702eb302d1237a4fd8e7d5ffadce2fedfee1d0cbd360efd84095407e5cb0fdd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

4 release files

0.5.0

4 release files

0.4.0

4 release files

0.3.0

4 release files

0.2.0

4 release files

0.1.0

4 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page