AWS Systems Manager · 2026-10-15 · SSM Operations arc
AWS SSM Operations for MCP Servers: Run Command, Session Manager, Maintenance Windows, and Patch Manager
The single most dangerous gap in an MCP server deployment is treating operational access as an afterthought — opening port 22, leaving SSH keys in a shared 1Password vault, and relying on ad-hoc restarts rather than scheduled maintenance — until the day a 3 AM API key compromise requires you to rotate credentials across 40 instances faster than the attacker can exfiltrate them. AWS Systems Manager eliminates all of those gaps with four tools that share a single prerequisite (the AmazonSSMManagedInstanceCore managed policy on the instance role) and compose into a coherent operational stack: Run Command for non-interactive fleet operations with rate controls and a full audit trail, Session Manager for interactive shell access and port-forwarding through a WebSocket relay that never opens an inbound port, Maintenance Windows for scheduled recurring tasks with declarative duration and cutoff semantics, and Patch Manager for OS-level CVE patching with compliance reporting that generates an audit record per instance. For MCP servers specifically — which hold API keys for downstream services with real spending and data access implications — each of these tools addresses a distinct threat model: Run Command eliminates the inbound-port attack surface; Session Manager eliminates bastion host maintenance and SSH key rotation; Maintenance Windows prevents the accumulation of state that causes silent outages (memory leaks, stale connections, growing caches); Patch Manager closes the OS-level paths a compromised web request could use to escalate to root and exfiltrate those API keys. Four topics composed into three structural patterns: the shared infrastructure and IAM model that underlies all four tools, the access control and audit design that makes the stack auditable, and the fleet operations patterns that connect scheduled maintenance to live incident response with AliveMCP monitoring as the external validation layer throughout.
TL;DR
- Shared infrastructure — one policy, four tools: Attach
AmazonSSMManagedInstanceCoreto every MCP server EC2 instance role. For private subnets without NAT, add three VPC Interface Endpoints:com.amazonaws.{region}.ssm,com.amazonaws.{region}.ssmmessages, andcom.amazonaws.{region}.ec2messages— withprivateDnsEnabled: true. Tag every instance withService=mcp-serverandEnv=productionat launch time in CDK. These tags are the targeting mechanism for all four SSM tools: Run Command targeting, Session Manager IAM condition keys, Maintenance Window target registration, and Patch Group assignment viaPatch Group=mcp-productiontag. Run Command usesAWS-RunShellScriptwith--targets '[{"Key":"tag:Service","Values":["mcp-server"]}]'; always setMaxConcurrency(use 25% for most operations, 1 for API key rotation) andMaxErrors(10% for most, 0 for key rotation). The Session Manager plugin for the AWS CLI is a local prerequisite — install withbrew install --cask session-manager-pluginon macOS; the instance needs no additional setup beyond the managed policy. For Maintenance Windows: create the window with a cron schedule, a duration in hours, and a cutoff (hours before window end to stop dispatching new tasks); attach an IAM service role with theAmazonSSMMaintenanceWindowRolemanaged policy and a trust policy forssm.amazonaws.com. For Patch Manager: the default AWS-managed baselines (Critical + Important, 7-day auto-approval delay) are sufficient for most MCP server deployments; create a customCfnPatchBaselineonly if you need to pin or exclude specific packages (e.g., blocking Node.js version bumps via the system package manager when you manage Node with nvm separately). RunAWS-RunPatchBaselinewithOperation=Scanat any time safely; runOperation=Installonly inside a Maintenance Window task to prevent uncontrolled reboots on the production fleet. - Access control and audit design: Scope Session Manager access by EC2 tag using the
ssm:resourceTag/ServiceIAM condition key — the policy'sAllowon instances withService=mcp-serverplus an explicitDenyon any instance without that tag prevents on-call engineers from accidentally sessioning into non-MCP infrastructure while preserving full access to every MCP instance regardless of instance ID. Enable Session Manager logging by creating anSSM-SessionManagerRunShelldocument that streams all session activity to a CloudWatch Log Group with 90-day retention — the document name must match exactly for automatic application. SetHISTFILE=/dev/nullin the shell profile to prevent commands from being written to instance bash history (protects against secrets leaking into other users'.bash_history) while CloudWatch captures everything at the session level for your security team. For Run Command: the caller IAM role needsssm:SendCommand,ssm:GetCommandInvocation, andssm:ListCommandInvocations; scope the resource ARN toarn:aws:ssm:*:*:document/mcp-*for custom documents to prevent operators from running arbitrary built-in documents on production instances. Patch compliance data is written to Systems Manager after every Scan or Install operation and queryable viaaws ssm list-compliance-items— use this as an audit artifact for security reviews. Every Run Command execution records the caller's IAM identity, exact document and parameters, and per-instance stdout/stderr output in CloudWatch Logs — this is the audit trail that post-incident reviews rely on to answer "what was run and by whom." - Fleet operations patterns — scheduled maintenance and incident response: Pair Maintenance Windows with AliveMCP monitoring to validate recovery: AliveMCP probes every 60 seconds during the window, so a rolling restart with 25% concurrency produces a predictable pattern (batch of instances go offline, recover, next batch goes offline) — aggregate probe success rate dips but never reaches zero if concurrency is correctly set. An instance that does not recover within 5 minutes of its restart means the maintenance operation failed on that instance — not just that it is slow. For patch operations, use 10% concurrency with
RebootIfNeeded(reboots only if the installed patches require it — kernel updates, glibc); setNoRebootonly to explicitly defer, accepting that the instance runs patched-but-not-active binaries until the next reboot. After patching, run a follow-up Scan to refresh compliance data (Install updates installed state; Scan updates compliance metadata — these are separate operations). For incident response: the workflow is AliveMCP alert fires → check which instances are failing → runaws ssm start-session --target i-{id}on the failing instance → checksystemctl status mcp-serverandjournalctl -u mcp-server -n 100→ fix or restart → AliveMCP probe recovers. The entire loop can run in under five minutes when Session Manager is already configured; without it, step 3 becomes 15 minutes of bastion and SSH key gymnastics while the outage compounds. For key rotation emergencies: use a custom Run Command document withMaxConcurrency=1andMaxErrors=0— rotate one instance, confirm it came back healthy on AliveMCP, proceed to the next.
Pattern 1 — The Shared Infrastructure Model
One policy, four tools, one CDK stack
Every SSM tool — Run Command, Session Manager, Maintenance Windows, Patch Manager — requires the same two things on the instance side: the SSM Agent running and the AmazonSSMManagedInstanceCore managed policy on the instance role. Both are free. The managed policy grants permission for the agent to register with the SSM service, receive commands, and write output to CloudWatch Logs and S3. It does not grant permission to call SSM APIs from application code — that is a separate role and a separate concern.
In CDK, the minimal MCP server instance setup that enables all four tools:
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as cdk from 'aws-cdk-lib';
const instanceRole = new iam.Role(this, 'McpInstanceRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
],
});
const instance = new ec2.Instance(this, 'McpServer', {
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL),
machineImage: ec2.MachineImage.latestAmazonLinux2023(), // SSM Agent pre-installed
role: instanceRole,
// No keyPair — SSM Session Manager replaces SSH
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
securityGroup: mcpSecurityGroup, // no inbound rules at all
});
// Tags are the universal targeting mechanism across all four SSM tools
cdk.Tags.of(instance).add('Service', 'mcp-server');
cdk.Tags.of(instance).add('Env', 'production');
cdk.Tags.of(instance).add('Patch Group', 'mcp-production'); // Patch Manager targeting
The Service=mcp-server and Env=production tags flow through to Run Command's --targets parameter, to Session Manager's IAM condition key scoping, to Maintenance Window target registration, and — via the Patch Group tag — to Patch Manager's baseline assignment. Define them once at instance launch and they work across all four tools without further configuration.
VPC endpoints for private subnets
MCP servers in private subnets without a NAT gateway cannot reach the SSM service endpoints over the public internet. Three VPC Interface Endpoints are required — and all three must be present; missing any one causes the SSM Agent to fail silently with no informative error to CloudWatch.
const ssmEndpoints = [
'com.amazonaws.us-east-1.ssm',
'com.amazonaws.us-east-1.ssmmessages',
'com.amazonaws.us-east-1.ec2messages',
];
for (const service of ssmEndpoints) {
new ec2.InterfaceVpcEndpoint(this, `Endpoint-${service.split('.').pop()}`, {
vpc,
service: new ec2.InterfaceVpcEndpointService(service),
subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
securityGroups: [endpointSecurityGroup],
privateDnsEnabled: true, // required — SSM Agent uses standard regional DNS names
});
}
privateDnsEnabled: true is mandatory. The SSM Agent resolves ssm.us-east-1.amazonaws.com using the standard regional DNS hostname. Without private DNS enabled on the endpoint, that name resolves to a public IP the instance cannot reach. The endpoint exists but the agent cannot find it. The symptom is the instance appearing as "Connection Lost" in Fleet Manager with no actionable error in instance logs.
The ssmmessages endpoint is the WebSocket relay used by Session Manager; ssm is the main API; ec2messages carries Run Command message delivery. All three are required regardless of which SSM tools you use — the agent needs all three endpoints to stay registered.
Pattern 2 — Access Control and the Audit Stack
Scoping Session Manager access by tag
By default, any IAM principal with ssm:StartSession on resource * can session into any SSM-managed instance in the account. For MCP server environments that hold downstream API keys, that default is wrong. Production MCP server instances should only be accessible to on-call engineers or a designated incident-response role. The ssm:resourceTag IAM condition key lets you enforce this at the policy level using the same tags you attached at instance launch.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSessionToMcpProductionOnly",
"Effect": "Allow",
"Action": [
"ssm:StartSession",
"ssm:TerminateSession",
"ssm:ResumeSession",
"ssm:DescribeSessions",
"ssm:GetConnectionStatus"
],
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ssm:*:*:document/AWS-StartPortForwardingSession"
],
"Condition": {
"StringEquals": {
"ssm:resourceTag/Service": "mcp-server",
"ssm:resourceTag/Env": "production"
}
}
},
{
"Sid": "DenySessionToNonMcpInstances",
"Effect": "Deny",
"Action": "ssm:StartSession",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ssm:resourceTag/Service": "mcp-server"
}
}
}
]
}
The explicit Deny in the second statement is necessary. Without it, a principal could have a separate broad Allow policy elsewhere in their attached roles and the condition on the first statement would not protect non-MCP instances. The Deny overrides any Allow — an engineer with this policy cannot session into a database instance, a build server, or a bastion host even if a separate policy would allow it.
This policy works regardless of instance count or instance ID. Tag every new MCP server instance at CDK deploy time and it automatically falls under the policy. Decommission an instance and remove its tags; the policy no longer covers it. No policy updates needed as the fleet scales.
Session logging: two controls, two audiences
For MCP servers handling sensitive downstream credentials, session logging serves two distinct purposes that require two separate controls. CloudWatch session streaming captures all session activity for your security team's audit and post-incident investigation. Setting HISTFILE=/dev/null in the shell profile prevents commands from writing to the instance's bash history, protecting against secrets leaking into files that other users on the same instance can read.
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as logs from 'aws-cdk-lib/aws-logs';
const sessionLogGroup = new logs.LogGroup(this, 'SsmSessionLogs', {
logGroupName: '/mcp/ssm-sessions',
retention: logs.RetentionDays.NINETY_DAYS,
});
// Document name must be exactly SSM-SessionManagerRunShell for automatic application
const sessionPreferences = new ssm.CfnDocument(this, 'SessionPreferences', {
name: 'SSM-SessionManagerRunShell',
documentType: 'Session',
content: {
schemaVersion: '1.0',
sessionType: 'InteractiveCommands',
inputs: {
cloudWatchLogGroupName: sessionLogGroup.logGroupName,
cloudWatchEncryptionEnabled: true,
cloudWatchStreamingEnabled: true,
idleSessionTimeout: '20',
maxSessionDuration: '60',
shellProfile: {
// Suppress instance-level history; CloudWatch captures everything at session level
linux: 'export HISTFILE=/dev/null; echo "Session on $(hostname) at $(date)"',
},
},
},
});
The document name SSM-SessionManagerRunShell is not arbitrary — this exact name is the default that SSM applies automatically to all sessions in the account. Name it anything else and you must explicitly specify it on each start-session call, which means engineers working from the CLI during an incident may bypass logging entirely. Use the exact name and the preferences apply to every session automatically.
The instance role also needs logs:CreateLogStream and logs:PutLogEvents on the session log group ARN. The AmazonSSMManagedInstanceCore managed policy does not include CloudWatch Logs write permissions — you must add them explicitly or sessions will connect successfully but log silently.
Run Command: caller audit trail and operator policy
Every Run Command invocation is recorded in CloudWatch Logs (when CloudWatchOutputConfig is configured) with the executing IAM identity, the exact document name and version, all parameters, and the full stdout/stderr output from each targeted instance. This is the audit trail that makes post-incident analysis tractable: "what command ran on which instance, in which order, by which IAM role, with what output."
The caller IAM policy for Run Command operators should scope document access to your own documents rather than granting access to all built-in documents:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:SendCommand",
"ssm:GetCommandInvocation",
"ssm:ListCommandInvocations",
"ssm:ListCommands"
],
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ssm:*:*:document/AWS-RunShellScript",
"arn:aws:ssm:*:*:document/AWS-RunPatchBaseline",
"arn:aws:ssm:*:*:document/mcp-*"
]
}
]
}
Scoping to arn:aws:ssm:*:*:document/mcp-* for custom documents means that an operator with this policy can run your approved runbooks but cannot run arbitrary AWS-RunShellScript commands outside of the explicitly listed built-in documents. For key rotation and config reload operations, this scoping matters: the runbook document encodes the approved procedure, and operators execute the runbook — not ad-hoc shell commands.
Pattern 3 — Fleet Operations: Scheduled Maintenance and Incident Response
Maintenance Windows: the scheduling model
A Maintenance Window is not an orchestration engine — it is a scheduling construct with four components: a cron or rate schedule, a duration in hours (how long the window stays open), a cutoff (how many hours before the window end to stop dispatching new tasks), and a set of tasks registered against tagged targets. The window knows when to open and when to stop dispatching; it does not know whether the tasks inside it succeeded. That validation is external — from Run Command's CloudWatch output and from AliveMCP's probe history.
The CDK stack for a weekly restart window:
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as iam from 'aws-cdk-lib/aws-iam';
// Weekly: Sunday 03:00 UTC, 2-hour window, stop dispatching 1 hour before close
const maintenanceWindow = new ssm.CfnMaintenanceWindow(this, 'McpMaintenanceWindow', {
name: 'mcp-server-weekly-maintenance',
schedule: 'cron(0 3 ? * SUN *)',
duration: 2,
cutoff: 1,
allowUnassociatedTargets: false,
enabled: true,
scheduleTimezone: 'UTC',
});
const windowTargets = new ssm.CfnMaintenanceWindowTarget(this, 'McpWindowTargets', {
windowId: maintenanceWindow.ref,
resourceType: 'INSTANCE',
targets: [
{ key: 'tag:Service', values: ['mcp-server'] },
{ key: 'tag:Env', values: ['production'] },
],
name: 'mcp-production-fleet',
});
const maintenanceRole = new iam.Role(this, 'MaintenanceWindowRole', {
assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonSSMMaintenanceWindowRole'),
],
});
const restartTask = new ssm.CfnMaintenanceWindowTask(this, 'McpRestartTask', {
windowId: maintenanceWindow.ref,
taskArn: 'AWS-RunShellScript',
taskType: 'RUN_COMMAND',
targets: [{ key: 'WindowTargetIds', values: [windowTargets.ref] }],
priority: 1,
maxConcurrency: '25%',
maxErrors: '20%',
serviceRoleArn: maintenanceRole.roleArn,
taskInvocationParameters: {
maintenanceWindowRunCommandParameters: {
parameters: {
commands: [
'systemctl restart mcp-server',
'sleep 10',
'systemctl is-active mcp-server && echo "RESTART_OK" || echo "RESTART_FAILED"',
],
},
cloudWatchOutputConfig: {
cloudWatchLogGroupName: '/mcp/maintenance',
cloudWatchOutputEnabled: true,
},
timeoutSeconds: 300,
},
},
});
Understanding cutoff behavior
Cutoff is the most commonly misunderstood Maintenance Window setting. It controls when SSM stops dispatching new task invocations — not when running invocations are killed. With a 2-hour window and 1-hour cutoff, tasks dispatched in the first hour continue until completion or their own TimeoutSeconds limit. Tasks not yet dispatched when the cutoff fires are cancelled.
For a rolling fleet restart, estimate the required window duration before creating the window:
const instanceCount = 100;
const concurrencyPercent = 0.25; // 25%
const batchSize = Math.ceil(instanceCount * concurrencyPercent); // 25 instances/batch
const minutesPerBatch = 3; // restart + 10s sleep + health check
const batches = Math.ceil(instanceCount / batchSize); // 4 batches
const totalMinutes = batches * minutesPerBatch; // 12 minutes total
// For a patch install with reboot (20 min/instance, 10% concurrency):
const patchInstancesPerBatch = Math.ceil(100 * 0.10); // 10 instances/batch
const patchBatches = 100 / patchInstancesPerBatch; // 10 batches
const patchTotalMinutes = patchBatches * 20; // 200 minutes
const patchDurationHours = Math.ceil(patchTotalMinutes / 60) + 1; // 5 hours
// Set duration=5, cutoff=1 for patch install windows
Setting the wrong duration is the most common reason a fleet emerges from a Maintenance Window with a subset of instances that never received the task — the cutoff fired before all batches were dispatched. For restarts (2–3 minutes each), a 2-hour window is more than sufficient for fleets under 1,000 instances. For OS patch installs that may trigger kernel updates and reboots (20–30 minutes each at 10% concurrency), size the window at approximately ceil((instances / concurrency_count) * minutes_per_instance / 60) + 1 hours.
Patch Manager: from compliance scan to install
Patch Manager operates in two modes. Scan mode reads the instance's installed packages against the approved patch baseline and writes compliance data to Systems Manager — it changes nothing on the instance. Install mode applies missing patches and reboots if required. The two modes are separate invocations and the compliance data is not automatically refreshed after an Install — you need a follow-up Scan to update the compliance view in the console.
Run a fleet-wide compliance scan at any time without disruption:
aws ssm send-command \
--document-name "AWS-RunPatchBaseline" \
--targets '[{"Key":"tag:Service","Values":["mcp-server"]}]' \
--parameters '{"Operation":["Scan"]}' \
--max-concurrency "100%" \
--cloud-watch-output-config '{"CloudWatchLogGroupName":"/mcp/patch-scans","CloudWatchOutputEnabled":true}' \
--output text \
--query "Command.CommandId"
100% concurrency is safe for Scan because it changes nothing. For Install, always use a Maintenance Window with 10% concurrency and never run Operation=Install directly with send-command on a production fleet — patching triggers reboots, and uncontrolled concurrent reboots take the entire fleet offline simultaneously.
For deployments where the default AWS-managed patch baselines are insufficient — you're managing Node.js with nvm and need to block the system package manager from upgrading it, or you're pinning a specific openssl version for a compliance requirement — create a custom CfnPatchBaseline with rejectedPatchesAction: 'BLOCK':
const mcpPatchBaseline = new ssm.CfnPatchBaseline(this, 'McpPatchBaseline', {
name: 'mcp-server-patch-baseline',
operatingSystem: 'AMAZON_LINUX_2023',
approvalRules: {
patchRules: [{
patchFilterGroup: {
patchFilters: [
{ key: 'CLASSIFICATION', values: ['Security', 'Bugfix'] },
{ key: 'SEVERITY', values: ['Critical', 'Important'] },
],
},
approveAfterDays: 7,
}],
},
// Block packages that would upgrade Node.js from the nvm-managed version
rejectedPatches: ['nodejs20', 'nodejs18'],
rejectedPatchesAction: 'BLOCK', // takes precedence over approval rules
patchGroups: ['mcp-production'], // matches the Patch Group tag value
});
BLOCK takes precedence over approval rules — if a rejected package appears in the approval criteria, it is still blocked. This is the correct behavior for pinned runtimes: you want security patches to install automatically, but not if they would silently upgrade a pinned runtime dependency. Use ALLOW_AS_DEPENDENCY if you want to allow a blocked package to install when pulled in as a dependency of an approved package (for transitive dependency scenarios).
The incident response workflow with AliveMCP
AliveMCP monitors MCP servers from outside the AWS account using a real JSON-RPC tools/list handshake every 60 seconds. When a probe starts failing, the incident workflow that SSM enables is faster than any SSH-based alternative:
- AliveMCP alert fires → on-call engineer is paged.
- Engineer opens AliveMCP dashboard → identifies which instance ID is failing and since when.
aws ssm start-session --target i-{id}— no bastion host, no VPN, no SSH key lookup. Session opens in under 10 seconds.- On the instance:
systemctl status mcp-server,journalctl -u mcp-server -n 100 --no-pager. - If the issue is a hung process or memory overflow:
systemctl restart mcp-server. - AliveMCP probe recovers (next 60-second interval confirms JSON-RPC response) → alert resolves.
- Post-mortem: pull the Run Command history or session log from CloudWatch to see whether this instance had the restart applied correctly in the last Maintenance Window.
For debugging a probe failure where the LB health check still shows the instance as healthy — a common AliveMCP detection pattern where the load balancer uses a shallow TCP check but AliveMCP's JSON-RPC probe is deeper — use port forwarding to reproduce exactly what AliveMCP sees from the instance's perspective:
# Forward localhost:13000 → instance:3000 (MCP server JSON-RPC port)
aws ssm start-session \
--target i-0abc123def456789a \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["3000"],"localPortNumber":["13000"]}'
# In a separate terminal: send the same probe AliveMCP sends
curl -s -X POST http://localhost:13000/ \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq .
If the port-forwarded probe returns a valid tools/list response but AliveMCP shows the instance as down, the failure is upstream of the instance (load balancer misconfiguration, security group change, Route 53 health check). If the port-forwarded probe also fails, the MCP server process itself is unhealthy — the LB is presenting a stale health check result from before the failure.
AliveMCP during scheduled maintenance
AliveMCP continues probing during Maintenance Windows by design. The probe validates that the maintenance operation actually recovered the server — not just that the maintenance script completed. During a rolling restart with 25% concurrency, the expected AliveMCP pattern is: batch of 25 instances go offline (probe failures), recover within 2–3 minutes (probe success), next batch goes offline, and so on. The aggregate probe success rate for the fleet dips during each batch but never reaches zero because 75% of instances are always running while the current batch restarts.
Two deviations from this expected pattern that warrant immediate investigation:
- Aggregate probe rate drops to zero — all instances offline simultaneously. This means
MaxConcurrencywas set to 100% or the Maintenance Window dispatched all batches before AliveMCP could confirm individual instance recovery. Check the Run Command execution history for the window's task ID in CloudWatch. - Individual instance does not recover within 5 minutes of its restart — the restart command succeeded (otherwise
MaxErrorswould have halted the window) but the MCP server process failed to start after the systemd restart. Use Session Manager to access the instance and runjournalctl -u mcp-server -n 50to see the startup failure reason.
After the Maintenance Window closes, verify that every instance in the AliveMCP dashboard has returned to green before considering maintenance complete. A single instance still failing after the window is a residual — not an expected post-maintenance state.
Failure modes consolidated
| Symptom | Tool | Cause | Fix |
|---|---|---|---|
| Instance shows "Connection Lost" in Fleet Manager | All SSM | SSM Agent not running; instance role missing AmazonSSMManagedInstanceCore; no route to SSM endpoints (private subnet, missing VPC endpoints) |
Check sudo systemctl status amazon-ssm-agent; verify instance profile; add three SSM VPC endpoints with privateDnsEnabled: true |
TargetNotConnected on StartSession |
Session Manager | Same as above — instance not registered in SSM; or SSM Agent stopped after a recent restart or OS update | Check Fleet Manager; if Agent is stopped, use EC2 Instance Connect (if available) or console-based serial access to restart it |
| Run Command succeeds but MCP server still uses old config | Run Command | SIGHUP reload not implemented in the service unit (ExecReload= not defined); or service restarted by systemd before new config was applied |
Add ExecReload= to the service unit file; or accept a brief restart window instead of reload |
| MaxErrors fires mid-fleet, remaining instances skip the task | Run Command / Maintenance Window | A subset of instances has a different OS version, missing dependency, or environment assumption in the script is wrong | Reduce MaxConcurrency to 1 for next attempt; inspect failed invocation stdout/stderr in CloudWatch; fix the script before rerunning |
| Maintenance Window shows scheduled but tasks never execute | Maintenance Window | No registered targets match at execution time (tag drift or instances not SSM-managed); window is disabled; schedule cron expression is invalid | Check "Targets" tab in the window console — must show at least one instance; verify Enabled=true; validate cron syntax in a cron tester |
| Fleet instances not fully patched when window closes | Maintenance Window + Patch Manager | Cutoff fired before all batches were dispatched; patch install per instance took longer than estimated (large patch set, slow reboot) | Increase window duration; reduce MaxConcurrency for patch installs; run a manual follow-up Maintenance Window for the missed instances |
| Patch compliance shows non-compliant after successful install | Patch Manager | Compliance data is not refreshed by Install — a follow-up Scan operation is required to update the compliance view | Run a follow-up Scan with Operation=Scan after the Install Maintenance Window completes |
| MCP server fails to start after kernel update | Patch Manager | Native Node.js addon compiled against old kernel headers; or Node.js path changed after package manager update | Use NoReboot for kernel-level patches in the main window; schedule kernel updates in a separate dedicated window with longer recovery testing |
| Session logs not appearing in CloudWatch | Session Manager | Session preferences document is not named exactly SSM-SessionManagerRunShell; or instance role lacks logs:CreateLogStream and logs:PutLogEvents |
Rename the document to the exact required name; add CloudWatch Logs write permissions to the instance role |
AccessDeniedException on Maintenance Window task execution |
Maintenance Window | Maintenance Window service role is missing permissions; trust policy must allow ssm.amazonaws.com to assume the role |
Attach service-role/AmazonSSMMaintenanceWindowRole; verify the trust policy includes ssm.amazonaws.com as principal |
Production checklist
Shared infrastructure
AmazonSSMManagedInstanceCoreattached to every EC2 instance roleService=mcp-server,Env=production, andPatch Group=mcp-productiontags on every instance at launch- Three SSM VPC Interface Endpoints deployed with
privateDnsEnabled: truefor private subnets - Session Manager plugin installed on every operator's workstation
Access control and audit
- Session Manager access policy uses
ssm:resourceTag/Servicecondition with explicit Deny on non-MCP instances - Session preferences document named exactly
SSM-SessionManagerRunShellwith CloudWatch streaming enabled - Instance role includes
logs:CreateLogStreamandlogs:PutLogEventsfor session log group - Run Command caller policy scoped to specific documents, not
*
Scheduled maintenance
- Maintenance Window duration estimated using the formula:
ceil((instance_count / concurrency_count) × minutes_per_task / 60) + 1 - Cutoff set to 1 hour (for 2-4 hour windows) to ensure in-progress tasks complete
- Restart task uses 25% concurrency; patch install uses 10% concurrency
- Post-patch follow-up Scan scheduled to refresh compliance data after Install window
- AliveMCP alert thresholds account for expected per-instance downtime during rolling restarts
- Post-window check: verify all instances return to green in AliveMCP dashboard before closing the incident