diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 1f232e4..f99aae7 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -315,7 +315,7 @@ pipelines: name: 01_image-setup script: - set -euo pipefail - - apt-get update -y && apt-get install -y openssh-client openjdk-21-jdk wget unzip curl + - apt-get update -y && apt-get install -y openssh-client openjdk-21-jdk wget unzip curl expect - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install @@ -324,6 +324,14 @@ pipelines: - echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key - ssh-keyscan -p "22" "${PROD_INSTANCE_IP}" >> ~/.ssh/known_hosts 2>/dev/null || true + - eval "$(ssh-agent -s)" + - | + expect -c " + spawn ssh-add ~/.ssh/sacc4_key + expect \"Enter passphrase\" + send \"${SSH_PASSPHRASE_THOTH}\r\" + expect eof + " - export TELEGRAM_BOT_TOKEN="${PROD_TELEGRAM_BOT_TOKEN}" - export TELEGRAM_CHAT_ID="${PROD_TELEGRAM_CHAT_ID}" - bash scripts/telegram-pipeline-notify.sh start @@ -423,9 +431,18 @@ pipelines: name: 06_update_ssh_keys script: - set -euo pipefail + - apt-get update -y && apt-get install -y expect - mkdir -p ~/.ssh - echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key + - eval "$(ssh-agent -s)" + - | + expect -c " + spawn ssh-add ~/.ssh/sacc4_key + expect \"Enter passphrase\" + send \"${SSH_PASSPHRASE_THOTH}\r\" + expect eof + " # Actualizar authorized_keys del usuario thoth con la llave pública del pipeline # Esto asegura que solo el pipeline actual pueda acceder, rotando llaves automáticamente - | @@ -441,7 +458,7 @@ pipelines: name: 07_install script: - set -euo pipefail - - apt-get update -y && apt-get install -y curl unzip + - apt-get update -y && apt-get install -y curl unzip expect - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install @@ -455,8 +472,16 @@ pipelines: fi if [ "${HAS_LOCAL_JAR}" = "true" ]; then echo "INFO: Artefacto JAR encontrado localmente. Procediendo con instalación en servidor." + mkdir -p ~/.ssh echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key chmod 600 ~/.ssh/sacc4_key + eval "$(ssh-agent -s)" + expect -c " + spawn ssh-add ~/.ssh/sacc4_key + expect \"Enter passphrase\" + send \"${SSH_PASSPHRASE_THOTH}\r\" + expect eof + " ssh -p "22" \ -i ~/.ssh/sacc4_key \ -o StrictHostKeyChecking=no \ @@ -483,7 +508,7 @@ pipelines: trigger: manual script: - set -euo pipefail - - apt-get update -y && apt-get install -y curl unzip + - apt-get update -y && apt-get install -y curl unzip expect - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" - unzip -q awscliv2.zip - ./aws/install @@ -501,6 +526,14 @@ pipelines: fi - echo "${SSH_PRIVATE_KEY_THOTH}" > ~/.ssh/sacc4_key - chmod 600 ~/.ssh/sacc4_key + - eval "$(ssh-agent -s)" + - | + expect -c " + spawn ssh-add ~/.ssh/sacc4_key + expect \"Enter passphrase\" + send \"${SSH_PASSPHRASE_THOTH}\r\" + expect eof + " - | ssh -p "22" \ -i ~/.ssh/sacc4_key \ diff --git a/terraform/environments/prod.tfvars b/terraform/environments/prod.tfvars index 9e66608..05748ff 100644 --- a/terraform/environments/prod.tfvars +++ b/terraform/environments/prod.tfvars @@ -13,8 +13,10 @@ aws_region = "mx-central-1" vpc_cidr = "10.2.0.0/16" availability_zones = ["mx-central-1a", "mx-central-1b"] ec2_instance_type = "t3.small" -ec2_key_name = "ccsoft-prod-key" -pipeline_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQCNFOzDJzaOMDIeEbH4JCx2OrXrgljajgkJqlozj9m bitbucket.pipeline.ci.cd.proyectosacc.thoth@computocontable.com" +ec2_ami = "ami-0f553e2869648134e" +ec2_key_name = "sacc-prod-key-2026" +ec2_root_volume_size = 8 +pipeline_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII/RcJmEYOBpfq1tSLltV1pyNB55l1jA2zYr5ZNJ0f41 thoth@ccsoft" db_instance_class = "db.t3.micro" db_name = "sacc_db_prod" db_username = "sacc_admin_prod" diff --git a/terraform/main.tf b/terraform/main.tf index 928e411..80e3f74 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -116,12 +116,8 @@ resource "aws_security_group" "ec2_api" { vpc_id = aws_vpc.main.id description = "Security Group para la API backend de ${var.project_name}" - # NOTA DE SEGURIDAD: Acceso SSH controlado EXCLUSIVAMENTE por llaves SSH - # administradas por el pipeline CI/CD (key-based auth), NO por restricción de IP. - # El pipeline inyecta y rota las llaves públicas en authorized_keys del usuario thoth. - # Considerar migrar a AWS Systems Manager Session Manager para eliminar acceso SSH directo. ingress { - description = "SSH - Acceso controlado por llaves CI/CD (no por IP)" + description = "SSH desde IPs confiables" from_port = 22 to_port = 22 protocol = "tcp" @@ -152,6 +148,41 @@ resource "aws_security_group" "ec2_api" { cidr_blocks = [aws_vpc.main.cidr_block] } + ingress { + from_port = 8081 + to_port = 8081 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } + + ingress { + from_port = 8082 + to_port = 8082 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } + + ingress { + from_port = 8083 + to_port = 8083 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } + + ingress { + from_port = 8084 + to_port = 8084 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } + + ingress { + from_port = 8085 + to_port = 8085 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } + egress { description = "Salida libre" from_port = 0 @@ -254,6 +285,23 @@ resource "aws_iam_role_policy" "ec2_policy" { "cloudfront:CreateInvalidation" ] Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "ssm:UpdateInstanceInformation", + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + "ec2messages:AcknowledgeMessage", + "ec2messages:DeleteMessage", + "ec2messages:FailMessage", + "ec2messages:GetEndpoint", + "ec2messages:GetMessages", + "ec2messages:SendReply" + ] + Resource = "*" } ] }) @@ -276,23 +324,35 @@ resource "aws_instance" "api" { instance_type = var.ec2_instance_type subnet_id = aws_subnet.public[0].id vpc_security_group_ids = [aws_security_group.ec2_api.id] - iam_instance_profile = aws_iam_instance_profile.ec2_profile.name key_name = var.ec2_key_name root_block_device { - volume_type = "gp3" - volume_size = var.ec2_root_volume_size - encrypted = true + volume_type = "gp2" + volume_size = 8 + encrypted = false delete_on_termination = true } - user_data = base64encode(templatefile("${path.module}/user-data.sh", { - pipeline_public_key = var.pipeline_public_key - })) - tags = { - Name = "${var.project_name}-api-${var.environment}" - Environment = var.environment + Name = "${var.project_name}-api-${var.environment}" + Environment = var.environment + AutoStart = "true" + AutoStop = "true" + Schedule = "office-hours" + ScheduleHours = "08:00-19:00_L-V" + ScheduleTimezone = "America/Mexico_City" + } + + lifecycle { + ignore_changes = [ + iam_instance_profile, + user_data, + tags["AutoStart"], + tags["AutoStop"], + tags["Schedule"], + tags["ScheduleHours"], + tags["ScheduleTimezone"], + ] } } @@ -326,8 +386,23 @@ resource "aws_db_instance" "main" { backup_retention_period = 7 tags = { - Name = "${var.project_name}-rds" - Environment = var.environment + Name = "${var.project_name}-rds" + Environment = var.environment + AutoStart = "true" + AutoStop = "true" + Schedule = "office-hours" + ScheduleHours = "08:00-19:00_L-V" + ScheduleTimezone = "America/Mexico_City" + } + + lifecycle { + ignore_changes = [ + tags["AutoStart"], + tags["AutoStop"], + tags["Schedule"], + tags["ScheduleHours"], + tags["ScheduleTimezone"], + ] } } diff --git a/terraform/user-data.sh b/terraform/user-data.sh index eb3dc71..63445ea 100755 --- a/terraform/user-data.sh +++ b/terraform/user-data.sh @@ -22,6 +22,32 @@ PIPELINE_PUBLIC_KEY="${pipeline_public_key}" apt-get update -y apt-get install -y nginx openjdk-21-jdk awscli curl jq +# ------------------------------------------------------------------------------- +# Instalar y verificar AWS Systems Manager Agent +# ------------------------------------------------------------------------------- +# SSM Agent permite acceso seguro sin abrir puertos SSH (0.0.0.0/0) +# Pre-instalado en Amazon Linux; en Ubuntu puede requerir instalación manual +apt-get install -y amazon-ssm-agent 2>/dev/null || true + +# Si el paquete no está disponible en repositorios, descargar desde AWS +if ! command -v amazon-ssm-agent &> /dev/null; then + echo "Descargando SSM Agent desde AWS..." + curl -fsSL -o /tmp/amazon-ssm-agent.deb https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb + dpkg -i /tmp/amazon-ssm-agent.deb || true + rm -f /tmp/amazon-ssm-agent.deb +fi + +# Asegurar que el servicio esté habilitado y corriendo +systemctl enable amazon-ssm-agent || true +systemctl restart amazon-ssm-agent || true + +# Verificar estado del agente +if systemctl is-active --quiet amazon-ssm-agent; then + echo "SSM Agent instalado y activo correctamente" +else + echo "ADVERTENCIA: No se pudo iniciar SSM Agent. Verificar conectividad a AWS y permisos IAM." +fi + # ------------------------------------------------------------------------------- # Crear usuarios del sistema # ------------------------------------------------------------------------------- @@ -38,6 +64,52 @@ echo "$PIPELINE_PUBLIC_KEY" > /home/thoth/.ssh/authorized_keys chmod 600 /home/thoth/.ssh/authorized_keys chown -R thoth:thoth /home/thoth/.ssh +# ------------------------------------------------------------------------------- +# Configurar permisos sudo para usuario thoth (SACC4 Application Management) +# ------------------------------------------------------------------------------- +cat > /etc/sudoers.d/thoth <<'SUDOERS_EOF' +# SACC4 Application Management Permissions +# User: thoth +# Generated: $(date) + +# 1. Editar archivo de configuracion de la aplicacion +thoth ALL=(ALL) NOPASSWD: /usr/bin/nano /etc/sacc4/sacc4.env +thoth ALL=(ALL) NOPASSWD: /usr/bin/vim /etc/sacc4/sacc4.env +thoth ALL=(ALL) NOPASSWD: /usr/bin/vi /etc/sacc4/sacc4.env + +# 2. Gestionar servicios systemd +thoth ALL=(ALL) NOPASSWD: /usr/bin/systemctl status api-sacc4-*.service +thoth ALL=(ALL) NOPASSWD: /usr/bin/systemctl start api-sacc4-*.service +thoth ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop api-sacc4-*.service +thoth ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart api-sacc4-*.service +thoth ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable api-sacc4-*.service +thoth ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable api-sacc4-*.service +thoth ALL=(ALL) NOPASSWD: /usr/bin/journalctl -u api-sacc4-*.service + +# 3. Editar archivos de servicios systemd +thoth ALL=(ALL) NOPASSWD: /usr/bin/nano /etc/systemd/system/api-sacc4-*.service +thoth ALL=(ALL) NOPASSWD: /usr/bin/vim /etc/systemd/system/api-sacc4-*.service +thoth ALL=(ALL) NOPASSWD: /usr/bin/vi /etc/systemd/system/api-sacc4-*.service + +# 4. Recargar systemd daemon +thoth ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload + +# 5. Control total del directorio /opt/sacc4 +thoth ALL=(ALL) NOPASSWD: /bin/ls -la /opt/sacc4/* +thoth ALL=(ALL) NOPASSWD: /bin/chown -R thoth\:thoth /opt/sacc4/* +thoth ALL=(ALL) NOPASSWD: /bin/chmod -R [0-7][0-7][0-7] /opt/sacc4/* +thoth ALL=(ALL) NOPASSWD: /bin/mkdir -p /opt/sacc4/* +thoth ALL=(ALL) NOPASSWD: /bin/rm -rf /opt/sacc4/* +thoth ALL=(ALL) NOPASSWD: /bin/cp -r * /opt/sacc4/* +thoth ALL=(ALL) NOPASSWD: /bin/mv * /opt/sacc4/* +SUDOERS_EOF + +chmod 440 /etc/sudoers.d/thoth +chown root:root /etc/sudoers.d/thoth + +# Validar sintaxis del archivo sudoers +visudo -c || echo "ADVERTENCIA: Error en sintaxis de /etc/sudoers.d/thoth" + # ------------------------------------------------------------------------------- # Crear estructura de directorios de despliegue # ------------------------------------------------------------------------------- diff --git a/terraform/variables.tf b/terraform/variables.tf index 20c2022..fbaf240 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -80,6 +80,12 @@ variable "pipeline_public_key" { type = string } +variable "allowed_ssh_cidrs" { + description = "Lista de CIDRs permitidos para acceso SSH (vacío = deshabilitado). Preferir AWS Systems Manager Session Manager en lugar de SSH." + type = list(string) + default = [] +} + # ------------------------------------------------------------------------------- # RDS (Base de datos) # -------------------------------------------------------------------------------