OpenTofu ile EC2 Instance Connect Kurmak

Selamlar, bu yazımda EC2 instance'larına SSH ile bağlanmanın bence en derli toplu yollarından birine, yani EC2 Instance Connect'e OpenTofu tarafından bakacağız. Klasik 'public IP ver, .pem dosyasını sakla, kaybetme aman' düzeninden sıkıldıysanız doğru yerdesiniz. Hadi başlayalım.

İşin özü şu: Instance Connect, AWS'in instance'a kısa ömürlü (60 saniye geçerli) bir SSH public key push etmesine izin veriyor. Yani siz .pem dosyası yönetmiyorsunuz, IAM yetkisi yönetiyorsunuz. Üstüne CloudTrail her bağlantı denemesini logluyor. Audit isteyen ekipler için bu tek başına bir gerekçe.

Instance Connect Endpoint nedir?

Geleneksel yaklaşımda private subnet'teki bir makineye ulaşmak için ya bastion host kuruyorduk ya da makineye public IP veriyorduk. İkisi de hoş değil; bastion bakım ister, public IP saldırı yüzeyi açar. Instance Connect Endpoint (kısaca EICE), VPC içinde duran managed bir tünel gibi düşünülebilir. Siz aws ec2-instance-connect ssh dediğinizde trafik bu endpoint'ten geçip private instance'a varıyor, üstelik ortada SSH portunu internete açmak yok.

Şahsi kanaatim, yeni bir VPC kuruyorsanız bastion'a bakmadan doğrudan EICE ile başlamak en mantıklısı. Tek bir endpoint, bütün VPC.

IAM policy: ince ayar burada

Instance Connect'in gücü, IAM koşullarıyla daraltabilmenizden geliyor. Aşağıdaki policy üç şeye izin veriyor: SSH key push etmek, endpoint'ten tünel açmak ve instance listelemek.

data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "instance_connect" {
  statement {
    sid     = "AllowInstanceConnect"
    effect  = "Allow"
    actions = ["ec2-instance-connect:SendSSHPublicKey"]
    resources = [
      "arn:aws:ec2:${var.region}:${data.aws_caller_identity.current.account_id}:instance/*"
    ]
    condition {
      test     = "StringEquals"
      variable = "ec2:osuser"
      values   = ["ec2-user"]
    }
  }

  statement {
    sid     = "AllowOpenTunnel"
    effect  = "Allow"
    actions = ["ec2-instance-connect:OpenTunnel"]
    resources = [aws_ec2_instance_connect_endpoint.private.arn]
    condition {
      test     = "NumericEquals"
      variable = "ec2-instance-connect:remotePort"
      values   = ["22"]
    }
  }
}

resource "aws_iam_policy" "instance_connect" {
  name   = "EC2InstanceConnectPolicy"
  policy = data.aws_iam_policy_document.instance_connect.json
}

ec2:osuser koşuluna dikkat edin: kullanıcının hangi OS user ile bağlanabileceğini IAM tarafında kilitliyoruz. Yani biri root olarak girmek istese bile policy ona izin vermiyor. Production ortamında bunu mutlaka set edin.

Endpoint ve security group tarafı

Endpoint'in kendisi tek bir kaynak. Güvenlik grubu da yine tek bir egress kuralından ibaret olabilir; VPC CIDR'ine 22 portu yetiyor.

resource "aws_ec2_instance_connect_endpoint" "private" {
  subnet_id          = var.private_subnet_id
  security_group_ids = [aws_security_group.eice.id]
  preserve_client_ip = true

  tags = { Name = "eice-private" }
}

resource "aws_security_group" "instance" {
  name   = "instance-ssh-sg"
  vpc_id = var.vpc_id

  ingress {
    description     = "SSH from EICE"
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = [aws_security_group.eice.id]
  }
}

Buradaki kritik nokta şu: instance'ın security group'unda SSH ingress'ini IP block ile değil, endpoint'in security group ID'si ile açıyoruz. Böylece SSH yalnızca EICE üzerinden gelen trafiğe açık, başka hiçbir kaynaktan değil.

Instance'ı key pair olmadan başlatmak

İşin tatlı tarafı, key_name parametresine ihtiyaç kalmıyor. Tofu tarafında bir blok eksiliyor:

resource "aws_instance" "app" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t3.micro"
  subnet_id              = var.private_subnet_id
  vpc_security_group_ids = [aws_security_group.instance.id]

  tags = { Name = "no-keypair-instance" }
}

Ardından bağlanmak için CLI'dan tek komut yetiyor:

aws ec2-instance-connect ssh \
  --instance-id i-0123456789abcdef0 \
  --os-user ec2-user \
  --connection-type eice \
  --region eu-central-1

tofu init && tofu plan && tofu apply ile değişiklikleri uygulayın, sonrasında ekipteki herkes kendi IAM kimliğiyle bağlanır. Anahtar dosyası dolaşmaz.

Sık karşılaşılan tuzaklar

  • AMI'da Instance Connect yok: AL2023 ve güncel Ubuntu'larda hazır geliyor; eski custom AMI'lerde ec2-instance-connect paketini elle kurmanız gerekebilir. Eksikse bağlantı sessizce timeout'a düşer.
  • preserve_client_ip ile NACL çatışması: Client IP korunduğunda subnet NACL'leri kendi public IP'nizden gelen trafiği engelleyebilir. Test ederken NACL'lere bir bakın.
  • ec2:osuser koşulunu unutmak: Policy'de bu koşul yoksa kullanıcı root veya başka bir OS user ile gelmeyi deneyebilir. Hep set edin.
  • Endpoint'i public subnet'e koymak: EICE zaten managed; private subnet'te durması gerekiyor, bunun için public route'a gerek yok.

Kapanış

Bu yazıda EC2 Instance Connect Endpoint'i OpenTofu ile uçtan uca kurduk; IAM koşullarıyla erişimi sıkılaştırmanın ve bastion'sız çalışmanın nasıl mümkün olduğunu gördük. Bana sorarsanız, yeni VPC'lerde artık varsayılan yaklaşım bu olmalı. Umarım faydalı olur, görüşmek üzere.