{"id":4690,"date":"2025-11-08T16:48:04","date_gmt":"2025-11-08T16:48:04","guid":{"rendered":"https:\/\/geekmungus.co.uk\/?p=4690"},"modified":"2025-11-08T16:48:24","modified_gmt":"2025-11-08T16:48:24","slug":"vpc-endpoints-for-ssm-without-internet-access","status":"publish","type":"post","link":"https:\/\/geekmungus.co.uk\/?p=4690","title":{"rendered":"VPC Endpoints for SSM without Internet Access"},"content":{"rendered":"\n<p>AWS SSM Web (Services System Manager), is a way to manage your EC2 instances via the SSM tool, this provides a number of different features, the one I commonly use is SSM for reaching the console of an EC2 instance; without needing SSH. With SSM you can get to the console and a command line, all without having to provide Internet access to the EC2 instance, and that is what this article is about.<\/p>\n\n\n\n<p>SSM requires that the EC2 instance (assuming it has the SSM Agent installed) to be able to reach the SSM endpoints to register and therefore be remotely controlled (if you are using the console access). If your EC2 instance can reach the Internet this is no problem, but what if your EC2 instance is on a private subnet with no Internet access, what then?<\/p>\n\n\n\n<p>Well, you can use a VPC Endpoint and publish the various needed endpoints directly within the VPC (and any subnets), what this means is that when the EC2 instance&#8217;s SSM Agent attempts to register, it will resolve the endpoint names but instead of attempting to reach these via the Internet, it will &#8220;short-circuit&#8221; its way directly from the VPC to the endpoint. Thus allowing it to work without Internet access.<\/p>\n\n\n\n<p>Here&#8217;s the Terraform for how to enable the VPC Endpoint for SSM:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>locals {\n  example_services2 = {\n    \"ec2messages\" : {\n      \"name\" : \"com.amazonaws.${var.region}.ec2messages\"\n    },\n    \"ssm\" : {\n      \"name\" : \"com.amazonaws.${var.region}.ssm\"\n    },\n    \"ssmmessages\" : {\n      \"name\" : \"com.amazonaws.${var.region}.ssmmessages\"\n    }\n  }\n}\n\nresource \"aws_vpc_endpoint\" \"example_ssm_endpoint2\" {\n  for_each            = local.example_services2\n  vpc_id              = aws_vpc.test_workload_2.id\n  service_name        = each.value.name\n  vpc_endpoint_type   = \"Interface\"\n  security_group_ids  = &#91;aws_security_group.sg_test_workload_2-Instance-1.id]\n  private_dns_enabled = true\n  ip_address_type     = \"ipv4\"\n  subnet_ids          = &#91;aws_subnet.test_workload_2-Private-Subnet-A.id, aws_subnet.test_workload_2-Private-Subnet-B.id]\n}\n<\/code><\/pre>\n\n\n\n<p>You&#8217;ll notice in that you need to create a Security Group to govern the traffic that can reach the SSM endpoints and also specify which subnets the VPC Endpoints will be published into.<\/p>\n\n\n\n<p>For reference, here is a more complete example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Security Group ---------------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"sg_test_workload_2-Instance-1\" {\n  name   = \"sg_test_workload_2-Instance-1\"\n  vpc_id = aws_vpc.test_workload_2.id\n\n  ingress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = &#91;\"0.0.0.0\/0\"]\n  }\n\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = &#91;\"0.0.0.0\/0\"]\n  }\n\n  lifecycle {\n    create_before_destroy = true\n  }\n}\n\n\/\/ EC2 Instances ----------------------------------------------------------------------------------------------------------------------------\n\n# User data: install nginx, make self-signed cert, HTTPS server block + redirect\nlocals {\n  example_user_data2 = &lt;&lt;-EOF\n    #!\/bin\/bash\n    set -eux\n    dnf -y install httpd\n    cat >\/var\/www\/html\/index.html &lt;&lt;'HTML'\n    &lt;!doctype html>\n    &lt;html>&lt;head>&lt;meta charset=\"utf-8\">&lt;title>Hello from EC2&lt;\/title>&lt;\/head>\n    &lt;body style=\"font-family: system-ui; margin: 3rem\">\n      &lt;h1>It works \ud83c\udf89&lt;\/h1>\n      &lt;p>Served by EC2 directly with nothing else!&lt;\/p>\n    &lt;\/body>&lt;\/html>\n    HTML\n    systemctl enable --now httpd\n  EOF\n}\n\n\/\/ EC2 Instance 1\n\nresource \"aws_instance\" \"test_workload_2-Instance-1\" {\n  ami                    = \"ami-008ea0202116dbc56\"\n  instance_type          = \"t2.micro\"\n  tenancy                = \"default\"\n  availability_zone      = \"eu-west-2a\"\n  key_name               = \"\"\n  subnet_id              = aws_subnet.test_workload_2-Private-Subnet-A.id\n  vpc_security_group_ids = &#91;aws_security_group.sg_test_workload_2-Instance-1.id]\n  iam_instance_profile   = aws_iam_instance_profile.ec2_profile2.name\n  user_data              = local.example_user_data2\n\n  tags = {\n    Name = \"test_workload_2-Instance-1\"\n  }\n}\n\n\/\/ EC2 IAM Role -----------------------------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_iam_role\" \"ec2_role2\" {\n  name = \"ec2-ssm-role2\"\n\n  assume_role_policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = &#91;\n      {\n        Sid    = \"\"\n        Action = \"sts:AssumeRole\"\n        Effect = \"Allow\"\n        Principal = {\n          Service = \"ec2.amazonaws.com\"\n        }\n      },\n    ]\n  })\n}\nresource \"aws_iam_role_policy_attachment\" \"custom2\" {\n  role       = aws_iam_role.ec2_role2.name\n  policy_arn = \"arn:aws:iam::aws:policy\/AmazonSSMManagedInstanceCore\"\n}\nresource \"aws_iam_instance_profile\" \"ec2_profile2\" {\n  name = \"ec2-ssm-profile2\"\n  role = aws_iam_role.ec2_role2.name\n}\n\n# # \/\/ VPC Endpoint (for SSM) -------------------------------------------------------------------------------------------------------\n\nlocals {\n  example_services2 = {\n    \"ec2messages\" : {\n      \"name\" : \"com.amazonaws.${var.region}.ec2messages\"\n    },\n    \"ssm\" : {\n      \"name\" : \"com.amazonaws.${var.region}.ssm\"\n    },\n    \"ssmmessages\" : {\n      \"name\" : \"com.amazonaws.${var.region}.ssmmessages\"\n    }\n  }\n}\n\nresource \"aws_vpc_endpoint\" \"example_ssm_endpoint2\" {\n  for_each            = local.example_services2\n  vpc_id              = aws_vpc.test_workload_2.id\n  service_name        = each.value.name\n  vpc_endpoint_type   = \"Interface\"\n  security_group_ids  = &#91;aws_security_group.sg_test_workload_2-Instance-1.id]\n  private_dns_enabled = true\n  ip_address_type     = \"ipv4\"\n  subnet_ids          = &#91;aws_subnet.test_workload_2-Private-Subnet-A.id, aws_subnet.test_workload_2-Private-Subnet-B.id]\n}<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>AWS SSM Web (Services System Manager), is a way to manage your EC2 instances via the SSM tool, this provides a number of different features, the one I commonly use is SSM for reaching the console of an EC2 instance; without needing SSH. With SSM you can get to the console and a command line, &#8230; <a title=\"VPC Endpoints for SSM without Internet Access\" class=\"read-more\" href=\"https:\/\/geekmungus.co.uk\/?p=4690\" aria-label=\"Read more about VPC Endpoints for SSM without Internet Access\">Read more<\/a><\/p>\n","protected":false},"author":4,"featured_media":4205,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[28,3],"tags":[],"class_list":["post-4690","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aws","category-cloud"],"_links":{"self":[{"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/posts\/4690","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4690"}],"version-history":[{"count":1,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/posts\/4690\/revisions"}],"predecessor-version":[{"id":4691,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/posts\/4690\/revisions\/4691"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=\/wp\/v2\/media\/4205"}],"wp:attachment":[{"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4690"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4690"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/geekmungus.co.uk\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4690"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}