aws-ssm-bridge
A Rust implementation of the AWS Systems Manager Session Manager protocol, with async Python bindings.
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 anyunsafefails 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 withSession::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)
| File | Size | Uploaded | |
|---|---|---|---|
| aws_ssm_bridge-0.6.0.tar.gz | 354.4 kB | Details |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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 logRelease 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 logRelease 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