AWS Systems Manager: Reference and Hardening
AWS Systems Manager: Reference and Hardening
AWS Systems Manager on one sheet, for the people who run it and the people who audit it: how the SSM Agent connects, the three ways to onboard a node, Session Manager and its preferences, Run Command, State Manager, Automation, Patch Manager, Parameter Store and just-in-time node access, then a seven-layer hardening model with a control matrix, operator IAM, node hardening, detection, an SCP and the changes of 2025 and 2026. Nine diagrams, every point checked against the AWS documentation as of October 2026. The tickable companion is bubim-awsssm-run.
What Systems Manager is
A set of tools that run commands, sessions, patches and configuration on managed nodes: EC2 instances, on-premises servers, VMs in other clouds and edge devices that run the SSM Agent. AWS renamed the capabilities “tools” in January 2025, and the unified console now shows nodes across every account and Region of an organization.
fig 1 · the node tools are what most teams use daily; dashed boxes mark tools closed to new customers
| You need to | Use | Instead of |
|---|---|---|
| open a shell on a node with no inbound port and no key | Session Manager | SSH or RDP through a bastion |
| reach a private database or web console from a laptop | Session Manager port forwarding | a VPN or a public endpoint |
| run a command on 500 nodes now | Run Command with rate controls | a for loop over SSH |
| keep a setting true on every node, forever | a State Manager association | a cron job copied by hand |
| run a multi-step fix with an approval step | an Automation runbook | a wiki page |
| patch on a schedule and prove it | a Patch Manager patch policy | unattended upgrades with no report |
| store configuration and small secrets | Parameter Store; Secrets Manager when rotation is needed | environment files on disk |
| know what is installed where | Inventory with a resource data sync | spreadsheets |
| grant node access only when approved | just-in-time node access | standing StartSession permissions |
| manage servers outside AWS | hybrid activations, or a Cloud Connector for Azure | a second tool chain |
How the agent connects
The SSM Agent runs as root on Linux and as SYSTEM on Windows. It calls the Systems Manager endpoints over HTTPS and keeps a control channel open; commands and sessions arrive on that channel. Nothing ever connects to the node, so security groups need no inbound rule at all.
fig 2 · nothing ever connects in: the agent polls ssm and holds a channel open on ssmmessages
| Endpoint | Carries | Needed when |
|---|---|---|
| ssm | registration, the heartbeat (UpdateInstanceInformation every 5 minutes), documents, inventory, compliance | always |
| ssmmessages | the control channel that keeps the agent connected, session data channels, and Run Command from agent 3.3.40.0 | always |
| ec2messages | the older message delivery path for commands | agents older than 3.3.40.0 in Regions launched before 2024; Regions launched in 2024 or later support only ssmmessages |
| kms | session encryption, SecureString decryption on the node | kmsKeyId is set in the preferences, or the node reads SecureString values |
| logs | session transcripts and command output to CloudWatch Logs | cloudWatchLogGroupName is set |
| s3 (gateway) | session logs, command output, Amazon Linux repositories, agent and Distributor packages | logging to S3, patching, agent updates |
Agent lifeline ssmmessages:OpenControlChannel is what keeps a node connected. Remove it from the node role and the node drops off within about an hour, the same as deleting the role. Once every agent is 3.3.40.0 or later, ec2messages:* can go from node policies.
fig 3 · two interface endpoints are the minimum; add kms and logs when the session preferences use them
| Private connectivity setting | Value |
|---|---|
| VPC attributes | enableDnsSupport and enableDnsHostnames on, private DNS on each interface endpoint |
| Endpoint security group | inbound 443 from the VPC CIDR (or the node security groups); no other rule |
| Endpoint policy | allow only principals of your organization (aws:PrincipalOrgID), so a stolen key from elsewhere cannot use your endpoint |
| S3 gateway endpoint | attached to the private route tables; its policy must still allow the AWS-owned buckets for agent updates and patch repositories |
| Proxy instead of endpoints | set http_proxy, https_proxy and no_proxy=169.254.169.254 for the agent service, then restart it |
Onboarding nodes
A machine is a managed node when its agent is registered and holds credentials that allow ssm:UpdateInstanceInformation. Those credentials come from one of three places.
fig 4 · DHMC is the default for EC2 at scale; activations are for machines outside EC2
| Method | Scope | Requirements | Notes |
|---|---|---|---|
| Instance profile | one instance | a role with AmazonSSMManagedInstanceCore | wins over DHMC when it allows ssm:UpdateInstanceInformation; replace the deprecated AmazonEC2RoleforSSM, which grants broad S3 access |
| Default Host Management Configuration (DHMC) | every EC2 instance in one account and Region | IMDSv2, agent 3.2.582.0 or later, role AWSSystemsManagerDefaultEC2InstanceManagementRole with AmazonSSMManagedEC2InstanceDefaultPolicy | one service setting; up to 30 minutes to apply; the role covers every instance, so add logging and KMS rights there and never application rights |
| Unified console setup | every account in the organization | management account to enable, a delegated administrator to run it, a home Region | keeps DHMC on daily, updates the agent every 14 days and collects inventory every 12 hours by default; accounts cannot be excluded; the home Region is fixed and must match IAM Identity Center |
| Hybrid activation | one machine outside EC2 | activation code and ID, a service role that ssm.amazonaws.com assumes | registration limit 1 by default, up to 1,000; expires after 24 hours by default, at most 30 days; node IDs start with mi- |
| Cloud Connector | Azure VMs at scale | a connector to the Azure subscription | installs the agent and enrolls new VMs; no per-node tier fee since July 2026 |
| Node shows as | Meaning | First check |
|---|---|---|
| Online | heartbeat received in the last few minutes | nothing to do |
| ConnectionLost | no heartbeat; network, role or agent problem | ssm-cli get-diagnostics on the node |
| not listed | agent never registered | role or DHMC, IMDSv2, route to the ssm endpoint, agent running |
| Stopped | EC2 instance stopped; counts as unmanaged in node insights | expected |
Session Manager
Browser or CLI shells, port forwarding and SSH tunnels to managed nodes, authorized by IAM and recorded in CloudTrail. The CLI needs the Session Manager plugin. Every session ID begins with the caller’s user ID, which is how policies limit terminate and resume to the caller’s own sessions.
fig 5 · the default shell document is granted with StartSession unless the policy forces an explicit document
Preferences: the SSM-SessionManagerRunShell document
One Session document per account and Region holds the preferences for every standard session. Whoever can update it can turn logging off, so lock it with an SCP (see Guardrails).
| Input | Effect | Hardened value |
|---|---|---|
| s3BucketName, s3KeyPrefix | transcript written to S3 when the session ends | a dedicated bucket with SSE-KMS, Block Public Access and a TLS-only policy |
| s3EncryptionEnabled | refuse to write to an unencrypted bucket | true |
| cloudWatchLogGroupName | transcript sent to CloudWatch Logs | a KMS-encrypted group with a retention period |
| cloudWatchEncryptionEnabled | refuse an unencrypted log group | true |
| cloudWatchStreamingEnabled | stream JSON events while the session runs instead of one upload at the end | true, so detections fire during the session |
| kmsKeyId | session data encrypted with your key on top of TLS | a customer managed key; users need kms:GenerateDataKey, nodes need kms:Decrypt |
| runAsEnabled, runAsDefaultUser | Linux and macOS sessions start as an OS user instead of ssm-user | true, with a low-privilege default user and an SSMSessionRunAs tag per principal |
| idleSessionTimeout | ends a session with no input | 1 to 60 minutes, default 20; 15 or less |
| maxSessionDuration | hard stop for any session | 1 to 1,440 minutes, no limit by default; 60 to 240 |
| shellProfile | commands run when a session starts | a banner, umask 027, TMOUT, HISTTIMEFORMAT |
Secrets in transcripts Logged sessions capture what is typed and printed. Read passwords with stty -echo; read passwd; stty echo on Linux or Read-Host -AsSecureString on Windows, and install screen on Linux images that lack it, or long transcripts can be truncated.
Session documents
| Document | Does | Logging | Grant to |
|---|---|---|---|
| SSM-SessionManagerRunShell | interactive shell, and the preferences | full transcript | operators, by node tag |
| AWS-StartInteractiveCommand | one interactive command, for example a database client | transcript | wrap it in your own document with the command fixed |
| AWS-StartNonInteractiveCommand | one command, output streamed back | transcript | same: fix the command |
| AWS-StartPortForwardingSession | a local port to a port on the node | none | a small group |
| AWS-StartPortForwardingSessionToRemoteHost | through the node to another host, such as RDS | none | a custom copy with host and port fixed; otherwise the node is a pivot to anything it can reach |
| AWS-StartSSHSession | SSH through the tunnel, as a ProxyCommand | none | only where scp, rsync or other SSH tooling is required; keys are still needed |
# shell, then a tunnel from localhost:5432 to a private database through the node
aws ssm start-session --target i-0abc1234def567890 --document-name SSM-SessionManagerRunShell
aws ssm start-session --target i-0abc1234def567890 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["db.internal.example.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'The implicit document A principal allowed ssm:StartSession on a node can also use SSM-SessionManagerRunShell, even if the policy lists only a port forwarding document. Add the condition “BoolIfExists”: {“ssm:SessionDocumentAccessCheck”: “true”} so a request must name a document the policy allows.
Run Command and SSM documents
Run Command sends a Command document to many nodes at once. AWS-RunShellScript and AWS-RunPowerShellScript run as root or SYSTEM, so permission to send them to a node is administrator access to that node.
| Option | send-command flag | Use |
|---|---|---|
| Targets | –targets Key=tag:Role,Values=web | tags or resource groups instead of instance ID lists, so new nodes are included |
| Concurrency | –max-concurrency 10% | a number or a percentage of targets at a time |
| Error threshold | –max-errors 1 | stop sending to more nodes after this many failures |
| Delivery timeout | –timeout-seconds 600 | how long a node may take to pick the command up; the script timeout is the executionTimeout parameter |
| Full output | –output-s3-bucket-name, –cloud-watch-output-config | the console and the API return truncated output; send it to S3 or CloudWatch Logs |
| Notifications | –notification-config, –service-role-arn | SNS on success, failure, timeout or cancel |
| Pin the content | –document-hash, –document-version | for shared documents: the call fails if the content has changed |
| Document type | Run by |
|---|---|
| Command | Run Command, State Manager, maintenance windows; schema 2.2 |
| Automation | Automation runbooks; schema 0.3 |
| Session | Session Manager: shell, ports, commands |
| Package | Distributor |
| ChangeCalendar | Change Calendar open and closed periods |
| Safe document habit | Why |
|---|---|
| interpolationType: ENV_VAR on String parameters | the value arrives as SSM_name, a literal, not script text; agent 3.3.2746.0 or later |
| allowedValues or allowedPattern | validated by the API and again by the agent |
| no SecureString in parameters | documents cannot reference SecureString; fetch it inside the script with the node role |
| share with accounts through AWS RAM | never publicly; block public sharing per Region |
restart-unit.json: a narrow Command document instead of granting AWS-RunShellScript
{
"schemaVersion": "2.2",
"description": "Restart one allowed systemd unit",
"parameters": {
"unit": {
"type": "String",
"description": "Unit to restart",
"allowedValues": ["nginx", "php-fpm", "amazon-cloudwatch-agent"],
"interpolationType": "ENV_VAR"
}
},
"mainSteps": [{
"action": "aws:runShellScript",
"name": "restart",
"precondition": {"StringEquals": ["platformType", "Linux"]},
"inputs": {"runCommand": ["systemctl restart \"$SSM_unit\"", "systemctl is-active \"$SSM_unit\""]}
}]
}State Manager, Automation, windows and calendars
| Tool | Holds | Runs | Hardening note |
|---|---|---|---|
| State Manager | associations: document, targets, schedule, parameters | at creation, then on a cron or rate schedule; –apply-only-at-cron-interval skips the first run | association compliance feeds Security Hub SSM.3; –calendar-names holds it during closed periods |
| Automation | runbooks (schema 0.3) with steps such as aws:executeAwsApi, aws:executeScript, aws:approve | on demand, from EventBridge, Config remediation, maintenance windows, associations | runs as an AutomationAssumeRole, so scope that role; send aws:executeScript output to CloudWatch (SSM.6); runtimes Python 3.10 and 3.11, PowerShell 7.4 |
| Multi-account Automation | admin role AWS-SystemsManager-AutomationAdministrationRole, target role AWS-SystemsManager-AutomationExecutionRole | across accounts and Regions, with concurrency and error limits | the execution role in each account is powerful: trust only the admin account |
| Maintenance windows | schedule, duration, cutoff, registered targets and tasks | Run Command, Automation, Lambda or Step Functions tasks inside the window | give tasks a service role, not the caller’s rights; log output to S3 |
| Change Calendar | a ChangeCalendar document, open or closed by default, with events | checked by runbooks and associations before they act | a freeze that automation respects; EventBridge sees state changes |
| Quick Setup | configurations deployed with CloudFormation StackSets | host management, DHMC, patch policies, scheduling, conformance packs | its roles are broad: create them from the delegated administrator only |
Patch Manager
Patch policies in Quick Setup are the recommended way to patch: one policy covers every account and Region of an organization, or selected OUs, or one account. Maintenance windows, standalone associations and “Patch now” still work, but only for one account and Region at a time.
fig 6 · scan often and install on a schedule; compliance is judged against the baseline, not against the vendor repo
| Baseline setting | Meaning | Typical choice |
|---|---|---|
| Approval rules | product, classification and severity filters, plus ApproveAfterDays or an approve-until date | security, critical and important after 7 days; everything else after 14 |
| Compliance level | severity reported when an approved patch is missing | CRITICAL for security rules, MEDIUM for the rest |
| Non-security updates (Linux) | EnableNonSecurity includes them from the repositories | off for prod, on for dev |
| Approved and rejected lists | explicit packages or KBs; rejected with BLOCK or ALLOW_AS_DEPENDENCY | block what broke you last time |
| Default baseline | AWS predefined per OS, or yours registered as default | your own, so a rule change is a reviewed change |
| AWS-RunPatchBaseline | Values | Note |
|---|---|---|
| Operation | Scan, Install | scan daily for compliance data, install in a window |
| RebootOption | RebootIfNeeded, NoReboot | with NoReboot, installed-pending-reboot patches keep the node non-compliant until it restarts |
| BaselineOverride | a baseline JSON in S3 | test a new baseline on a few nodes |
| InstallOverrideList | a patch list in S3 | emergency fixes outside the baseline |
Patch groups The Patch Group tag is needed only in account and Region pairs that used it before patch policies arrived on 22 December 2022. New setups use patch policies and never need it.
Parameter Store
Hierarchical key-value storage for configuration and small secrets. String, StringList and SecureString (encrypted with KMS). Every change is a new version, and labels point at versions.
fig 7 · paths make permissions cheap: grant a prefix, and bind the KMS grant to the parameter it decrypts
| Limit | Standard throughput | Higher throughput |
|---|---|---|
| GetParameter, GetParameters, GetParametersByPath | 40 TPS shared by all three | 10,000, 1,000 and 100 TPS |
| PutParameter, DescribeParameters, GetParameterHistory, labels | 3 TPS | 10 TPS |
| DeleteParameter, DeleteParameters | 3 TPS | 5 TPS |
| Cost | free for standard parameters | $0.05 per 10,000 API interactions, all parameters |
| Need | Parameter Store | Secrets Manager |
|---|---|---|
| Automatic rotation | no; an expiration policy can only notify | built in, with Lambda rotation for RDS, Redshift, DocumentDB and custom |
| Cross-account access | advanced parameters shared through AWS RAM | resource policies |
| Size | 4 KB or 8 KB | 64 KB |
| Price | standard free; advanced $0.05 per parameter-month | per secret per month plus API calls |
| Reach one from the other | GetParameter /aws/reference/secretsmanager/NAME –with-decryption reads a secret through Parameter Store | |
# a SecureString with your key; read it back; latest Amazon Linux 2023 AMI from a public parameter
aws ssm put-parameter --name /prod/payments/db/password --type SecureString \
--key-id alias/ssm-params-prod --value "$(openssl rand -base64 24)" --tier Standard
aws ssm get-parameter --name /prod/payments/db/password --with-decryption --query Parameter.Version
aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--query Parameter.Value --output textInventory, compliance, Fleet Manager, operations
| Tool | Gives you | Security use |
|---|---|---|
| Inventory | applications, AWS components, files, network configuration, services, Windows updates, roles and registry, custom types; collected by an association | find vulnerable versions fleet-wide; a resource data sync to S3 plus Athena answers “where is package X” |
| Compliance | patch, association and custom compliance per node | evidence for audits; keep ssm:PutComplianceItems off node roles so a node cannot mark itself compliant |
| Fleet Manager | file browser, users and groups, performance counters, event logs, registry, browser RDP | each view runs documents on the node: grant it like Session Manager |
| Distributor | AWS packages and your own, installed with AWS-ConfigureAWSPackage | roll out the CloudWatch agent or EDR sensors by association |
| Explorer, OpsCenter | an operations dashboard; OpsItems from alarms, Config, Security Hub and EventBridge | route findings to an owner with a runbook attached |
| Application Manager, Change Manager, Incident Manager | closed to new customers (July 2026, November 2025, November 2025) | do not build new processes on them |
Just-in-time node access
Zero standing access to nodes: operators request access with a justification, policies decide, and an approved request gets a time-bound token for a Session Manager or RDP session. Launched April 2025; it needs the unified console and is enabled per account, OU or organization.
fig 8 · zero standing access: operators hold StartAccessRequest, not StartSession
| Policy | Language | How many | Behaviour |
|---|---|---|---|
| Deny-access | Cedar | one per organization, from the delegated administrator | evaluated first; blocks auto-approval for the nodes and principals it names |
| Auto-approval | Cedar | one per account and Region | access for exactly 1 hour, not configurable |
| Manual approval | form: tags or all nodes, levels, approvers per level, duration | several; one per tag | access for 1 to 336 hours; tag-specific beats all-nodes; two policies matching one node conflict and block requests |
| Item | Detail |
|---|---|
| Operator permissions | ssm:StartAccessRequest, ssm:GetAccessToken, plus TerminateSession and ResumeSession; remove ssm:StartSession to finish the move |
| Notifications | email, and Slack or Microsoft Teams through Amazon Q Developer in chat applications; status events to EventBridge |
| Recording | shell sessions follow the session preferences; RDP sessions can be recorded to S3 with a KMS key |
| Scope | same account and Region as the requester; hybrid-activated nodes are supported |
| Cost | a free trial when first enabled, then per node-hour, from $0.0137 down to $0.0014 by volume |
auto-approval policy: developers reach development nodes without waiting for a human
permit (
principal in AWS::IdentityStore::Group::"REPLACE_WITH_DEV_GROUP_ID",
action == AWS::SSM::Action::"getTokenForInstanceAccess",
resource
)
when {
resource.hasTag("Environment") && resource.getTag("Environment") == "development"
};Hardening model
Systems Manager is a remote administration plane: anyone who can send a command or open a session is an administrator of the nodes it reaches. Harden it like a bastion, in layers, so one mistake does not open everything. The runbook turns every row into steps.
fig 9 · each layer stands on its own; the hardening matrix below gives the check for every chip
| Layer | Control | Why | Check |
|---|---|---|---|
| org | SCP: only the platform role may change SSM-SessionManagerRunShell | whoever edits the preferences can switch logging and encryption off | get-document SSM-SessionManagerRunShell |
| org | SCP: lock service settings | they hold the DHMC role, public sharing, Automation logging and Parameter Store throughput | get-service-setting |
| org | SCP: only the platform role may create activations | an activation lets any outside machine register and use the service role | describe-activations |
| org | Security Hub SSM.1 to SSM.7 and the matching Config rules | unmanaged instances, patch and association drift, public documents, missing logging | Security Hub findings |
| identity | StartSession only on tagged nodes and named documents | least privilege per team and environment | IAM Access Analyzer policy checks |
| identity | ssm:SessionDocumentAccessCheck true | closes the implicit shell that comes with any StartSession grant | a port-forward-only user cannot open a shell |
| identity | terminate and resume only session/${aws:userid}-* | one operator cannot take over another’s session | policy review |
| identity | SendCommand, associations, runbooks, documents and service settings for a few roles only | each is administrator access to nodes or to the plane itself | simulate-principal-policy |
| identity | just-in-time node access for production | no standing access; every session has a request, a reason and an approver | no human role holds ssm:StartSession on prod |
| node | DHMC role with the default policy plus logging and KMS rights only | the role is on every instance, and IMDS hands it to any process there | DHMC service setting |
| node | no AmazonEC2RoleforSSM or admin policies on instance profiles | broad S3 or admin rights turn a foothold into an account takeover | list-attached-role-policies |
| node | IMDSv2 required, as the account default | DHMC needs it; it blunts SSRF credential theft | ec2 get-instance-metadata-defaults |
| node | no sudo for ssm-user, enforced by an association; Run As per operator | a shell is not root by default, and the OS log shows who acted | sudo -l -U ssm-user |
| node | agent auto-update every 14 days or less | fixes, ssmmessages-only messaging, ENV_VAR support | AgentVersion in describe-instance-information |
| node | no ssm:PutComplianceItems on node roles | a compromised node could report itself compliant | policy review |
| node | hybrid: confused-deputy conditions on the service role, single-use activations, key rotation | activation codes leak; machine keys age | KeyAutoRotateDays |
| network | no inbound 22 or 3389 anywhere | Session Manager replaces both | Config restricted-ssh |
| network | interface endpoints ssm and ssmmessages (plus kms, logs) and an S3 gateway | private subnets with no NAT can still be managed | describe-vpc-endpoints |
| network | endpoint policies limited to your organization; endpoint SG 443 from the VPC | credentials stolen from another org cannot use your endpoints | endpoint policy review |
| session | KMS key in the preferences | a second encryption layer, and revoking key access stops new sessions | kmsKeyId |
| session | logs to S3 and CloudWatch, encryption required, streaming on | a full transcript, available while the session still runs | preferences document |
| session | idle 15 minutes, maximum 60 to 240 | no forgotten open shells | preferences document |
| session | port forwarding and SSH documents only to a small group; custom documents with the host fixed | these sessions are not logged, and the remote-host form pivots | policy review |
| content | block public sharing of documents in every Region | a public document can leak internals; SSM.7 | public-sharing-permission setting |
| content | ENV_VAR interpolation and allowed values in your documents | parameters cannot inject shell commands | document review |
| content | SecureString with a key per environment, Decrypt bound to PARAMETER_ARN | dev roles cannot read prod secrets even with a wide GetParameter grant | key policy review |
| content | Automation script output to CloudWatch; command output to encrypted S3 | evidence of what ran; SSM.6 | customer-script-log-destination setting |
| detect | organization CloudTrail with EventBridge alerts on SSM control-plane changes | someone disabling logging is itself the alert | test event |
Operator IAM
A shell policy for one team: nodes tagged ssm-access=ops, the shell document only, the caller’s own sessions, and the session KMS key. Attach it to an IAM Identity Center permission set.
ssm-operator-shell.json
{
"Version": "2012-10-17",
"Statement": [
{"Sid": "ShellOnTaggedNodes", "Effect": "Allow", "Action": "ssm:StartSession",
"Resource": ["arn:aws:ec2:*:111122223333:instance/*", "arn:aws:ssm:*:111122223333:managed-instance/*"],
"Condition": {"StringEquals": {"ssm:resourceTag/ssm-access": "ops"},
"BoolIfExists": {"ssm:SessionDocumentAccessCheck": "true"}}},
{"Sid": "ShellDocumentOnly", "Effect": "Allow", "Action": "ssm:StartSession",
"Resource": "arn:aws:ssm:*:111122223333:document/SSM-SessionManagerRunShell",
"Condition": {"BoolIfExists": {"ssm:SessionDocumentAccessCheck": "true"}}},
{"Sid": "OwnSessionsOnly", "Effect": "Allow",
"Action": ["ssm:TerminateSession", "ssm:ResumeSession", "ssmmessages:OpenDataChannel"],
"Resource": "arn:aws:ssm:*:*:session/${aws:userid}-*"},
{"Sid": "SeeNodes", "Effect": "Allow",
"Action": ["ssm:DescribeSessions", "ssm:GetConnectionStatus", "ssm:DescribeInstanceProperties",
"ssm:DescribeInstanceInformation", "ec2:DescribeInstances"],
"Resource": "*"},
{"Sid": "SessionKey", "Effect": "Allow", "Action": "kms:GenerateDataKey",
"Resource": "arn:aws:kms:eu-central-1:111122223333:key/REPLACE_WITH_SESSION_KEY_ID"}
]
}| Action | Equals | Keep it to |
|---|---|---|
| ssm:SendCommand | root or SYSTEM on every node it can target, through AWS-RunShellScript or any document the caller can create | pipelines and the platform team, by tag and by document ARN |
| ssm:CreateAssociation, UpdateAssociation | persistent code execution on a schedule | platform team |
| ssm:StartAutomationExecution + iam:PassRole | everything the runbook role can do | named runbooks, named roles |
| ssm:CreateDocument, UpdateDocument | changing what trusted documents run, including the session preferences | platform team, by document name |
| ssm:UpdateServiceSetting | swapping the DHMC role, opening public sharing, changing logging | nobody but the platform role (SCP) |
| ssm:CreateActivation + iam:PassRole | enrolling an outside machine with your service role | platform role (SCP) |
| ssm:RegisterTaskWithMaintenanceWindow | running tasks as the window’s service role | platform team |
| ssm:GetParameter* + kms:Decrypt | reading secrets | the workload role, by path |
| ssm:PutParameter | changing what applications read, such as an endpoint or a flag | deploy pipelines; deny overwrite with ssm:Overwrite where possible |
| ssm:ModifyDocumentPermission | sharing documents out of the account | nobody but the platform role (SCP) |
Node hardening
| Item | Default | Hardened |
|---|---|---|
| ssm-user on Linux and macOS | created at the first session (agent 2.3.612.0 and later) with ALL=(ALL) NOPASSWD:ALL in /etc/sudoers.d/ssm-agent-users | the line replaced by a comment, enforced by a State Manager association so a reinstall cannot bring it back unnoticed |
| ssm-user on Windows | member of the local Administrators group | net localgroup “Administrators” “ssm-user” /delete, by association |
| Run As | off | on, with a default low-privilege user; tag each IAM role or Identity Center user with SSMSessionRunAs; the OS user must exist |
| Agent configuration | /etc/amazon/ssm/amazon-ssm-agent.json.template | copy to amazon-ssm-agent.json only to change something; restart the agent after every change |
| Hybrid machine key | no rotation unless configured | Profile.KeyAutoRotateDays, agent 3.0.1031.0 or later |
| Registration files | /var/lib/amazon/ssm, C:\ProgramData\Amazon | leave them: without them a DHMC node cannot get credentials |
| Agent logs | /var/log/amazon/ssm/, %PROGRAMDATA%\Amazon\SSM\Logs | ship them with the CloudWatch agent; they show every document the node ran |
| Agent package | installed from AWS | verify the RPM, Debian or installer signature, and the Session Manager plugin’s, with the published GPG key |
| Hibernation | the agent backs off when it cannot reach the service | expected on isolated nodes; it resumes when connectivity returns |
# on a Linux node: remove ssm-user's sudo grant and confirm
echo "#User rules for ssm-user" | sudo tee /etc/sudoers.d/ssm-agent-users
sudo visudo -cf /etc/sudoers.d/ssm-agent-users
sudo -l -U ssm-userDetection and audit
| CloudTrail event | Alert on |
|---|---|
| StartSession | prod nodes outside JIT, break-glass roles, port forwarding documents |
| SendCommand | shell documents against prod tags, new callers |
| UpdateDocument, UpdateDocumentDefaultVersion | any change to SSM-SessionManagerRunShell |
| UpdateServiceSetting, ResetServiceSetting | always |
| ModifyDocumentPermission | always; worst when shared with All |
| CreateActivation | always |
| DeregisterManagedInstance | a node removed from management |
| CreateAssociation, StartAutomationExecution | new persistence or powerful runbooks |
| Control | Config rule | Severity |
|---|---|---|
| SSM.1 EC2 managed by SSM | ec2-instance-managed-by-systems-manager | medium |
| SSM.2 patch compliant | ec2-managedinstance-patch-compliance-status-check | high |
| SSM.3 association compliant | ec2-managedinstance-association-compliance-status-check | low |
| SSM.4 documents not public | ssm-document-not-public | critical |
| SSM.5 documents tagged | ssm-document-tagged | low |
| SSM.6 Automation logging | ssm-automation-logging-enabled | medium |
| SSM.7 block public sharing | ssm-automation-block-public-sharing | critical |
ssm-control-plane.json: EventBridge pattern for changes that weaken the setup
{
"source": ["aws.ssm"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["ssm.amazonaws.com"],
"eventName": ["UpdateDocument", "UpdateDocumentDefaultVersion", "DeleteDocument",
"UpdateServiceSetting", "ResetServiceSetting", "ModifyDocumentPermission",
"CreateActivation", "DeregisterManagedInstance"]
}
}Session history The console keeps a history of every session for 30 days; the transcripts themselves live only where the preferences send them. Streamed CloudWatch logs take metric filters, for example on sudo su or curl 169.254.169.254.
Guardrails
One SCP for workload OUs keeps the control plane in the hands of a single platform role. Add the roles that Quick Setup and the unified console create to the exceptions before attaching it, or their daily DHMC and agent associations fail. Test in a sandbox OU first.
scp-ssm-guardrails.json
{
"Version": "2012-10-17",
"Statement": [
{"Sid": "LockSessionPreferences", "Effect": "Deny",
"Action": ["ssm:CreateDocument", "ssm:UpdateDocument", "ssm:DeleteDocument", "ssm:UpdateDocumentDefaultVersion"],
"Resource": "arn:aws:ssm:*:*:document/SSM-SessionManagerRunShell",
"Condition": {"ArnNotLike": {"aws:PrincipalArn": ["arn:aws:iam::*:role/PlatformSsmAdmin"]}}},
{"Sid": "LockServiceSettings", "Effect": "Deny",
"Action": ["ssm:UpdateServiceSetting", "ssm:ResetServiceSetting"], "Resource": "*",
"Condition": {"ArnNotLike": {"aws:PrincipalArn": ["arn:aws:iam::*:role/PlatformSsmAdmin",
"arn:aws:iam::*:role/AWS-QuickSetup-*"]}}},
{"Sid": "NoSharingOut", "Effect": "Deny", "Action": "ssm:ModifyDocumentPermission", "Resource": "*",
"Condition": {"ArnNotLike": {"aws:PrincipalArn": ["arn:aws:iam::*:role/PlatformSsmAdmin"]}}},
{"Sid": "NoNewActivations", "Effect": "Deny", "Action": "ssm:CreateActivation", "Resource": "*",
"Condition": {"ArnNotLike": {"aws:PrincipalArn": ["arn:aws:iam::*:role/PlatformSsmAdmin"]}}},
{"Sid": "ProtectProdParameters", "Effect": "Deny",
"Action": ["ssm:PutParameter", "ssm:DeleteParameter", "ssm:DeleteParameters", "ssm:LabelParameterVersion"],
"Resource": "arn:aws:ssm:*:*:parameter/prod/*",
"Condition": {"ArnNotLike": {"aws:PrincipalArn": ["arn:aws:iam::*:role/PlatformSsmAdmin",
"arn:aws:iam::*:role/deploy-prod"]}}}
]
}Pricing
| Item | On EC2 | Outside EC2 |
|---|---|---|
| Session Manager | no charge | $0.05 per session since 30 September 2026 |
| Run Command | no charge | $0.002 per invocation since 30 September 2026 |
| Registering nodes | no charge | no charge; the advanced-instances tier ended on 30 June 2026 |
| Patch Manager, State Manager, Inventory, Compliance, Fleet Manager | no charge | patching through Run Command is billed as Run Command |
| Just-in-time node access | per node-hour after the trial: $0.0137 for the first 72,000 hours, falling to $0.0014 at high volume | |
| Automation | $0.002 per step, plus $0.00003 per second of aws:executeScript | |
| Parameter Store | standard free; advanced $0.05 per parameter-month; higher throughput $0.05 per 10,000 interactions | |
| OpsCenter | $2.97 per 1,000 OpsItems, plus read requests | |
| Around it | interface endpoints per AZ-hour and per GB, KMS requests, CloudWatch Logs ingestion, S3 storage | |
Recent changes
| When | Change | What to do |
|---|---|---|
| Dec 2022 | patch policies in Quick Setup | stop creating patch groups |
| Feb 2023 | Default Host Management Configuration | retire per-instance SSM profiles where DHMC fits |
| Feb 2024 | agent 3.3.40.0 moves messaging to ssmmessages | drop the ec2messages endpoint and permissions once agents are current |
| Nov 2024 | unified console across an organization | pick the home Region with Identity Center in mind |
| Jan 2025 | capabilities renamed tools in the documentation | expect new names in console and docs |
| Feb 2025 | signature verification documented for the agent and the Session Manager plugin | verify packages in image pipelines |
| Apr 2025 | just-in-time node access, RDP recording | plan the move from standing StartSession |
| Jul 2025 | ENV_VAR interpolation in documents (agent 3.3.2746.0) | add it to every String parameter you own |
| Oct 2025 | Automation runtimes: Python 3.10 and 3.11, PowerShell 7.4; Patch Manager IPv6 | update aws:executeScript steps on older runtimes |
| Nov 2025 | Change Manager and Incident Manager closed to new customers | use an ITSM partner or your own tooling for new setups |
| Apr 2026 | Systems Manager CloudWatch Dashboard retired | use CloudWatch dashboards directly |
| Jun 2026 | advanced-instances tier removed; no 1,000-node limit for hybrid nodes | remove tier settings from scripts |
| Jul 2026 | Azure Cloud Connector; Application Manager closed to new customers; Automation alarm checks use the execution identity | give runbook roles the CloudWatch alarm permissions they now need |
| Sep 2026 | pay-as-you-go for Session Manager and Run Command outside EC2 | count hybrid sessions; prefer associations over repeated commands |
Checked against the AWS documentation for SSM Agent messaging endpoints, Default Host Management Configuration, the unified console, Session Manager preferences and logging, ssm-user permissions, document sharing and parameters, Patch Manager policies, Parameter Store tiers and throughput, just-in-time node access, hybrid activations, Security Hub Systems Manager controls and Systems Manager pricing.

0 comments