Crear un servidor SFTP usando AWS Transfer Family, S3 y Route53

Hello World 🙂

En el artículo de hoy vamos a poner en práctica un caso de uso muy interesante que he implementado en varias ocasiones: la creación de un servidor SFTP en AWS. Este tipo de servicio es común en las empresas para compartir archivos, ya sea internamente o con clientes. Para ello, utilizaremos AWS Transfer Family, un servicio totalmente administrado por AWS. Esto significa que solo debemos encargarnos de su configuración inicial y del control de acceso, ya que AWS se ocupa del mantenimiento y escalabilidad cuando sea necesario.

AWS Transfer Family es un servicio de transferencia seguro que le permite transferir archivos dentro y fuera de los servicios de almacenamiento de AWS. AWS Transfer Family ofrece soporte totalmente administrado para la transferencia de archivos a través de SFTP, AS2, FTPS y FTP directamente dentro y fuera de Amazon S3 o Amazon EFS.

Tomado de AWS

Lo interesante de este servicio es que, como almacenamiento, utiliza S3 o EFS. En esta entrada nos enfocaremos en S3, un servicio altamente escalable, duradero y rentable, ideal para almacenar grandes cantidades de datos de manera segura y con acceso sencillo desde cualquier parte del mundo. En el repositorio de GitHub les dejo todo lo necesario para configurarlo también con EFS como backend de almacenamiento. Además, utilizaremos Route53 para crear una Zona Privada y agregar algunos registros necesarios para el SFTP, con el objetivo de que podamos acceder a él a través de una URL amigable, como por ejemplo sftps3.example.com. Como siempre, usaremos una VPC como base, ya que es un recurso indispensable en la mayoría de nuestros casos de uso.

Dado que nuestra zona en Route53 será privada y solo accesible desde nuestra VPC, también desplegaremos una instancia EC2 con Linux donde instalaremos todo lo necesario para hacer copiar algún fichero desde la instancia al SFTP.

Aunque todo esto puede configurarse desde la consola de AWS, en este blog preferimos hacer uso de Terraform siempre que sea posible, ya que nos permite replicar y mejorar la infraestructura de manera sencilla y eficiente.

¡Así que, sin más preámbulos, comencemos!

Requisitos:

  • Tener una cuenta de AWS.
  • Tener instalado Terraform.

El proyecto estará organizado en una única carpeta, dentro de la cual definiremos todos los archivos de Terraform necesarios para crear los recursos.

1- Primero necesitamos exportar nuestras credenciales de AWS en nuestra consola local;

  • export AWS_ACCESS_KEY_ID="XXXXXXXXXX"
    export AWS_SECRET_ACCESS_KEY="XXXXXXXXXX"
  • Comprobamos que estemos conectados de forma correcta
    •  aws sts get-caller-identity

2- En el archivo 0-provider.tf, definiremos los proveedores que utilizaremos en este proyecto. Estos proveedores son esenciales, ya que permiten a Terraform interactuar con los distintos servicios de infraestructura en la nube necesarios para desplegar los recursos correctamente.

  • provider "aws" {
      region = local.region
      default_tags {
        tags = local.tags
      }
    }

3- En este archivo 01-vpc.tf, definiremos todo lo necesario para crear nuestra VPC, que servirá como la base de la infraestructura del proyecto. Aquí, definiremos subredes y demás componentes claves que permitirán una red segura y eficiente para los recursos que desplegaremos más adelante.

  • module "vpc" {
      source  = "terraform-aws-modules/vpc/aws"
      name = "main-module"
      cidr = "10.0.0.0/16"
      azs             = ["us-west-1a", "us-west-1b"]
      private_subnets = ["10.0.0.0/19", "10.0.32.0/19"]
      public_subnets  = ["10.0.64.0/19", "10.0.96.0/19"]
      enable_dns_hostnames = true
      enable_dns_support   = true
      enable_nat_gateway     = true
      single_nat_gateway     = true
      one_nat_gateway_per_az = false
      public_subnet_tags = {
        "Name" = "public-subnet"
      }
      private_subnet_tags = {
        "Name" = "private-subnet"
      }
    }

4- En el archivo 02-security.tf, configuraremos detalladamente los grupos de seguridad necesarios para el proyecto.

  • resource "aws_security_group" "ec2" {
      name        = "SG-EC2"
      description = "SG-EC2"
      vpc_id = module.vpc.vpc_id
      ingress {
         from_port = 0
         to_port = 0
         protocol = "-1"
         cidr_blocks = ["0.0.0.0/0"]
       }
      egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
      }
      tags = {
        Name = "SG-EC2"
      }
    }

5- En el archivo 03-ec2.tf, configuraremos todo lo necesario para crear nuestra instancia EC2.

  • resource "aws_instance" "linuxinstance" {
        count = 1
        # Cambiar el AMI ID si cambias de región
        ami = "ami-ID"
        instance_type = "t2.medium"
        iam_instance_profile = "${aws_iam_instance_profile.poc_profile.name}"
        subnet_id = module.vpc.public_subnets[0]
        associate_public_ip_address= true
        vpc_security_group_ids = [aws_security_group.ec2.id]
        key_name="demo" # Cambiar
        tags= {
            Name = "demo-linux-awstf"
        }
    }

6- En el archivo 04-iam.tf, definiremos los roles y políticas necesarias para otorgar los permisos adecuados a nuestra instancia EC2 y al usuario que se conectará al EFS. En el caso de la política AWSTransferSFTPS3AccessPolicy, es importante recordar actualizar el ARN del bucket, asegurándonos de que los permisos sean aplicados correctamente y se ajusten a nuestro entorno.

  • resource "aws_iam_role" "AWSTransferSFTPS3AccessRole" {
      name = "AWSTransferSFTPS3AccessRole"
      assume_role_policy = <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Action": "sts:AssumeRole",
          "Principal": {
            "Service": "transfer.amazonaws.com"
          },
          "Effect": "Allow",
          "Sid": ""
        }
      ]
    }
    EOF
    }
  • Nota: Si creamos un Transfer Family que va a usar un EFS como backed de almacenamiento, tenemos que adjuntarle las siguientes políticas al rol ‘AmazonElasticFileSystemClientFullAccess‘ y ‘AWSTransferConsoleFullAccess‘. Si no adjuntamos estas políticas cuando intentemos conectarnos, obtendremos un error de acceso denegado.
    # Actualizar el ARN del Bucket
    resource "aws_iam_role_policy" "AWSTransferSFTPS3AccessPolicy" {
      name = "AWSTransferSFTPS3AccessPolicy"
      role = "${aws_iam_role.AWSTransferSFTPS3AccessRole.id}"
      policy = <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "s3:*",
          "Resource": [
              "arn:aws:s3:::poc-us-west-1-eks-pod-log",
              "arn:aws:s3:::poc-us-west-1-eks-pod-log/*"
          ]
        }
      ]
    }
    EOF
    }
    resource "aws_iam_role" "poc_role" {
      name = "poc_role"
      assume_role_policy = <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Action": "sts:AssumeRole",
          "Principal": {
            "Service": "ec2.amazonaws.com"
          },
          "Effect": "Allow",
          "Sid": ""
        }
      ]
    }
    EOF
    }
    resource "aws_iam_role_policy" "demo_policy" {
      name = "demo_policy"
      role = "${aws_iam_role.poc_role.id}"
      policy = <<EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "ec2:*",
          "Resource": "*"
        }
      ]
    }
    EOF
    }
    resource "aws_iam_policy_attachment" "attach_amazon_ssm_full_access-poc_role" {
      name       = "attach-amazon-ssm-full-access-poc_role"
      roles      = [aws_iam_role.poc_role.name]
      policy_arn = "arn:aws:iam::aws:policy/AmazonSSMFullAccess"
    }
    resource "aws_iam_policy_attachment" "attach_amazon_ec2_role_for_ssm-poc_role" {
      name       = "attach-amazon-ec2-role-for-ssm-poc_role"
      roles      = [aws_iam_role.poc_role.name]
      policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
    }
    resource "aws_iam_instance_profile" "poc_profile" {
      name = "poc_profile"
      role = "${aws_iam_role.poc_role.name}"
    }

7- En el archivo 05-locals.tf, definiremos las variables locales del proyecto. Estas variables ayudan a centralizar valores que se utilizarán en múltiples recursos, simplificando la configuración y facilitando el mantenimiento del código.

  • locals {
      region = "us-west-1"
      environment = "poc"
      bucket_name  = "${local.environment}-${local.region}-sftp"
      tags = {
        Environment   = "POC",
        Terraform     = "True"
      }
    }

8- En el archivo 06-rt53.tf, vamos a definir nuestra zona privada local en Route53 y vamos a crear el registro llamado sftps3.example.com.

  • esource "aws_route53_zone" "private" {
      name = "example.com"
      vpc {
        vpc_id = module.vpc.vpc_id
      }
    }
    resource "aws_route53_record" "sftp_dns_alias" {
      zone_id = aws_route53_zone.private.zone_id
      name    = "sftps3.example.com"
      type    = "CNAME"
      ttl     = 300
      records = [aws_transfer_server.sftp_transfer_server.endpoint]
    }

9- En el archivo 07-s3.tf, vamos a definir lo necesario para crear nuestro bucket de S3

  • resource "aws_s3_bucket" "demo" {
      bucket = local.bucket_name
      tags = {
        Name = local.bucket_name
      }
    }

10- En el archivo 08-sftp-with-s3.tf, definiremos todos los recursos necesarios para configurar el servidor SFTP, incluyendo el grupo de logs en CloudWatch (cloudwatch_log_group) que registrará la actividad del SFTP, permitiéndonos monitorear eventos y posibles problemas, entre otros.

  • resource "aws_transfer_server" "sftp_transfer_server" {
      identity_provider_type = "SERVICE_MANAGED"
      endpoint_type          = "PUBLIC"
      protocols              = ["SFTP"] # Especificamos el protocolo SFTP
      domain                 = "S3"     # Especificamos S3 como el sistema de almacenamiento
      security_policy_name   = "TransferSecurityPolicy-2024-01"
      post_authentication_login_banner = "Bienvenido al SFTP usando S3"
      logging_role  = aws_iam_role.iam_for_transfer.arn
      structured_log_destinations = [
        "${aws_cloudwatch_log_group.transfer.arn}:*"
      ]
      tags = {
        Name = "SFTP-S3-Server"
      }
    }
    resource "aws_transfer_tag" "hostname" {
      resource_arn = aws_transfer_server.sftp_transfer_server.arn
      key          = "aws:transfer:customHostname"
      value        = "sftps3.example.com"
    }
    resource "aws_cloudwatch_log_group" "transfer" {
      name_prefix = "transfer_test_"
    }
    data "aws_iam_policy_document" "transfer_assume_role" {
      statement {
        effect = "Allow"
        principals {
          type        = "Service"
          identifiers = ["transfer.amazonaws.com"]
        }
        actions = ["sts:AssumeRole"]
      }
    }
    resource "aws_iam_role" "iam_for_transfer" {
      name_prefix         = "iam_for_transfer_"
      assume_role_policy  = data.aws_iam_policy_document.transfer_assume_role.json
      managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AWSTransferLoggingAccess"]
    }

11- En el archivo 09-aws_tranfer_user-s3.tf, vamos a definir lo necesario para crear nuestro usuario que se va a conectar al EFS.

  • resource "aws_transfer_user" "sftp_s3_user" {
      server_id   = aws_transfer_server.sftp_transfer_server.id
      user_name   = "s3_user"
      role        = aws_iam_role.AWSTransferSFTPS3AccessRole.arn
      home_directory = "/${aws_s3_bucket.demo.bucket}/s3_user"
    }

Con todos los archivos configurados, ya estamos listos para ejecutar Terraform y desplegar la infraestructura.

  • terraform init
  • terraform plan
  • terraform apply

Vamos a comprobar que todos recursos fueron generados correctamente, conectarnos a la instancia Linux, hacer algunos ajustes y finalmente copiar algún fichero hacia el EFS.

  • Verificamos que el servicio de SFTP este activo y que el usuario esté creado.
  • Zona privada y registro creados en Route53.
  • Revisamos que la politica AWSTransferSFTPS3AccessPolicy tenga el ARN del bucket correcto.
  • Nos conectamos a la instancia EC2 y generamos una llave SSH.
  • Nota: Copiar esa llave y actualizar el parámetro body dentro de aws_transfer_ssh_key_s3.
    • Copiar el siguiente código en el fichero 09-aws_tranfer_user-s3.tf y volver a ejecutar terraform apply, de esta forma nos aseguramos de que el usuario que generemos tenga la llave SSH correcta.
      • resource "aws_transfer_ssh_key" "aws_transfer_ssh_key_s3" {
          server_id = aws_transfer_server.sftp_transfer_server.id
          user_name = aws_transfer_user.sftp_s3_user.user_name
          # salida del comando cat .ssh/id_rsa.pub
          body      = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDYMy...'"
        }
  • Después de eso ya podemos establecer conexión con el servidor SFTP usando el usuario creado.
    • Pero primero vamos a ejecutar nslookup y telnet para ver si la respuesta es correcta, es una forma sencilla de validar que llegamos al SFTP bien.
    • Ahora que todo parece estar bien, vamos a conectarnos usando el comando sftp s3_user@sftps3.example.com, aunque también podemos usar el comando scp para conectarnos y copiar cualquier fichero.
      • Como pueden ver usando el comando put podemos subir fichero desde nuestra instancia hacia el SFTP.
    • Usando el comando get podemos descargar ficheros del SFTP hacia nuestra instancia.
    • Podemos ver todas las opciones ejecutando el comando help.
    • Nota: Todo este proceso de verificación también se puede realizar desde una instancia con SO Windows. Solo necesitas instalar FileZilla, generar una clave SSH usando PuTTYgen e importarla en FileZilla. Este método también es sencillo e intuitivo, y de hecho, diría que es más utilizado que el procedimiento que describí usando la instancia con Linux.
  • Así podemos ver los ficheros en el bucket S3.
  • Podemos ver las traza en el Log group de CloudWatch creado.
    • Cuando accedemos a los logs guardados en CloudWatch podemos ver detalle sobre la conexión, ficheros copiados o algún otro problema que podamos tener.

Terraform destroy

  • Como siempre, al finalizar tus pruebas, recuerda eliminar todos los recursos creados ejecutando terraform destroy --auto-approve. Este paso es crucial para evitar sorpresas desagradables en tu factura a final del mes. No subestimes la importancia de esta tarea, ya que la acumulación de recursos no utilizados puede generar costos innecesarios. Tómate el tiempo para limpiar tu entorno, eso te ahorrará dolores de cabeza en el futuro 🙂

Conclusiones

Hemos llegado al final de esta entrada, en la cual vimos cómo crear un servidor SFTP en AWS, aprovechando las grandes ventajas que ofrece la plataforma. Fuimos capaces de combinar varios servicios clave para construir una infraestructura pequeña pero robusta, obteniendo un servicio para la transferencia de archivos, altamente demandado tanto para el trabajo interno de muchas organizaciones como para el intercambio de datos con sus clientes.

En esta ocasión, nos enfocamos en el uso de S3 como almacenamiento, destacando su escalabilidad y bajo costo, pero como mencioné al inicio, también es posible utilizar EFS, lo que puede ser más adecuado para ciertos escenarios. Ambos servicios son excelentes opciones, y la decisión entre uno u otro dependerá del caso de uso específico.

¡Nos vemos en la próxima entrada!

Nota: Todo el código lo pueden encontrar en el siguiente repositorio.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.