Skip to main content
Securing your ADMA application with HTTPS is essential for production deployments. AWS Certificate Manager (ACM) provides free SSL/TLS certificates with automatic renewal.

Prerequisites

Before requesting an ACM certificate:
  • Domain name: You must own a domain name (e.g., example.com)
  • DNS access: Ability to create DNS records for domain validation
  • ALB created: Your Application Load Balancer must exist
ACM certificates are free and automatically renew. You only pay for the resources using them (ALB).

Certificate Strategy

For the ADMA application, request a certificate that covers both your main domain and subdomains:
  • Primary domain: example.com (frontend)
  • API subdomain: api.example.com (backend)
  • Short URL subdomain: go.example.com (redirect domain)
Use a wildcard certificate or list specific SANs (Subject Alternative Names).

Request ACM Certificate

1
Choose Validation Method
2
ACM supports two validation methods:
3
  • DNS validation (recommended): Add a CNAME record to prove ownership
  • Email validation: Receive validation emails at admin addresses
  • 4
    Use DNS validation for automated renewals. Email validation requires manual intervention every 13 months.
    5
    Request Wildcard Certificate
    6
    export DOMAIN="example.com"  # Replace with your domain
    
    CERT_ARN=$(aws acm request-certificate \
      --domain-name $DOMAIN \
      --subject-alternative-names "*.$DOMAIN" \
      --validation-method DNS \
      --region $AWS_REGION \
      --query 'CertificateArn' \
      --output text)
    
    echo "Certificate ARN: $CERT_ARN"
    
    7
    This creates a certificate valid for:
    8
  • example.com
  • *.example.com (all subdomains)
  • 9
    Request Certificate with Specific Domains
    10
    Alternatively, list specific domains:
    11
    CERT_ARN=$(aws acm request-certificate \
      --domain-name $DOMAIN \
      --subject-alternative-names "api.$DOMAIN" "go.$DOMAIN" "www.$DOMAIN" \
      --validation-method DNS \
      --region $AWS_REGION \
      --query 'CertificateArn' \
      --output text)
    
    12
    Get Validation Records
    13
    Retrieve the DNS records you need to create:
    14
    aws acm describe-certificate \
      --certificate-arn $CERT_ARN \
      --region $AWS_REGION \
      --query 'Certificate.DomainValidationOptions[].ResourceRecord' \
      --output table
    
    15
    Expected output:
    16
    ----------------------------------------------------------------------
    |                      DescribeCertificate                           |
    +------------+-------------------------------------------------------+
    |    Name    |  _abc123.example.com                                 |
    |    Type    |  CNAME                                               |
    |    Value   |  _xyz456.acm-validations.aws.                        |
    +------------+-------------------------------------------------------+
    

    DNS Validation

    Validate with Route 53 (Automatic)

    If your domain uses Route 53, ACM can create validation records automatically:
    1
    Get Hosted Zone ID
    2
    HOSTED_ZONE_ID=$(aws route53 list-hosted-zones \
      --query "HostedZones[?Name=='$DOMAIN.'].Id" \
      --output text | cut -d'/' -f3)
    
    echo "Hosted Zone ID: $HOSTED_ZONE_ID"
    
    3
    Create Validation Records Automatically
    4
    # Get validation record details
    VALIDATION_RECORD=$(aws acm describe-certificate \
      --certificate-arn $CERT_ARN \
      --region $AWS_REGION \
      --query 'Certificate.DomainValidationOptions[0].ResourceRecord' \
      --output json)
    
    RECORD_NAME=$(echo $VALIDATION_RECORD | jq -r '.Name')
    RECORD_VALUE=$(echo $VALIDATION_RECORD | jq -r '.Value')
    
    # Create the CNAME record
    aws route53 change-resource-record-sets \
      --hosted-zone-id $HOSTED_ZONE_ID \
      --change-batch '{
        "Changes": [{
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "'$RECORD_NAME'",
            "Type": "CNAME",
            "TTL": 300,
            "ResourceRecords": [{"Value": "'$RECORD_VALUE'"}]
          }
        }]
      }'
    
    5
    Wait for Validation
    6
    Validation typically completes within 5-30 minutes:
    7
    aws acm describe-certificate \
      --certificate-arn $CERT_ARN \
      --region $AWS_REGION \
      --query 'Certificate.Status' \
      --output text
    
    8
    Wait until the status changes from PENDING_VALIDATION to ISSUED.

    Validate with External DNS Provider

    If you use a different DNS provider (Cloudflare, GoDaddy, Namecheap, etc.):
    1
    Get Validation Record Details
    2
    aws acm describe-certificate \
      --certificate-arn $CERT_ARN \
      --region $AWS_REGION \
      --query 'Certificate.DomainValidationOptions[].ResourceRecord'
    
    3
    Add CNAME Record Manually
    4
    In your DNS provider’s control panel:
    5
    FieldValueTypeCNAMEName_abc123.example.com (from ACM output)Value_xyz456.acm-validations.aws. (from ACM output)TTL300 (5 minutes)
    6
    Some DNS providers require you to enter only the subdomain part (e.g., _abc123) without the domain name.
    7
    Verify DNS Propagation
    8
    dig _abc123.example.com CNAME +short
    
    9
    Should return the ACM validation value.
    10
    Wait for ACM to Validate
    11
    ACM automatically checks DNS records every few minutes. Once validated, the certificate status changes to ISSUED.

    Configure ALB to Use Certificate

    Update Existing HTTPS Listener

    If you created an HTTPS listener without a certificate:
    # Get listener ARN
    LISTENER_ARN=$(aws elbv2 describe-listeners \
      --load-balancer-arn $ALB_ARN \
      --query 'Listeners[?Port==`443`].ListenerArn' \
      --output text \
      --region $AWS_REGION)
    
    # Add certificate
    aws elbv2 modify-listener \
      --listener-arn $LISTENER_ARN \
      --certificates CertificateArn=$CERT_ARN \
      --ssl-policy ELBSecurityPolicy-TLS-1-2-2017-01 \
      --region $AWS_REGION
    

    Create New HTTPS Listener

    If you only have an HTTP listener:
    # Delete temporary HTTP listener on port 80 if it forwards traffic
    OLD_LISTENER=$(aws elbv2 describe-listeners \
      --load-balancer-arn $ALB_ARN \
      --query 'Listeners[?Port==`80` && DefaultActions[0].Type==`forward`].ListenerArn' \
      --output text)
    
    if [ ! -z "$OLD_LISTENER" ]; then
      aws elbv2 delete-listener --listener-arn $OLD_LISTENER --region $AWS_REGION
    fi
    
    # Create HTTP to HTTPS redirect
    aws elbv2 create-listener \
      --load-balancer-arn $ALB_ARN \
      --protocol HTTP \
      --port 80 \
      --default-actions Type=redirect,RedirectConfig="{Protocol=HTTPS,Port=443,StatusCode=HTTP_301}" \
      --region $AWS_REGION
    
    # Create HTTPS listener
    LISTENER_ARN=$(aws elbv2 create-listener \
      --load-balancer-arn $ALB_ARN \
      --protocol HTTPS \
      --port 443 \
      --certificates CertificateArn=$CERT_ARN \
      --ssl-policy ELBSecurityPolicy-TLS-1-2-2017-01 \
      --default-actions Type=forward,TargetGroupArn=$FRONTEND_TG_ARN \
      --region $AWS_REGION \
      --query 'Listeners[0].ListenerArn' \
      --output text)
    
    echo "HTTPS Listener ARN: $LISTENER_ARN"
    

    Add Routing Rules to HTTPS Listener

    Re-create the routing rules if you created a new listener:
    # Route /api/* to backend
    aws elbv2 create-rule \
      --listener-arn $LISTENER_ARN \
      --priority 10 \
      --conditions Field=path-pattern,Values="/api/*" \
      --actions Type=forward,TargetGroupArn=$BACKEND_TG_ARN \
      --region $AWS_REGION
    
    # Route short codes to backend
    aws elbv2 create-rule \
      --listener-arn $LISTENER_ARN \
      --priority 20 \
      --conditions Field=path-pattern,Values="/???????" \
      --actions Type=forward,TargetGroupArn=$BACKEND_TG_ARN \
      --region $AWS_REGION
    

    Configure DNS for Your Domain

    Point Domain to ALB

    Create DNS records pointing to your ALB:
    # Create A record for root domain
    aws route53 change-resource-record-sets \
      --hosted-zone-id $HOSTED_ZONE_ID \
      --change-batch '{
        "Changes": [{
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "'$DOMAIN'",
            "Type": "A",
            "AliasTarget": {
              "HostedZoneId": "'$(aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN --query 'LoadBalancers[0].CanonicalHostedZoneId' --output text)'",
              "DNSName": "'$ALB_DNS'",
              "EvaluateTargetHealth": false
            }
          }
        }]
      }'
    
    # Create A record for api subdomain
    aws route53 change-resource-record-sets \
      --hosted-zone-id $HOSTED_ZONE_ID \
      --change-batch '{
        "Changes": [{
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "api.'$DOMAIN'",
            "Type": "A",
            "AliasTarget": {
              "HostedZoneId": "'$(aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN --query 'LoadBalancers[0].CanonicalHostedZoneId' --output text)'",
              "DNSName": "'$ALB_DNS'",
              "EvaluateTargetHealth": false
            }
          }
        }]
      }'
    
    # Create A record for go subdomain (short URLs)
    aws route53 change-resource-record-sets \
      --hosted-zone-id $HOSTED_ZONE_ID \
      --change-batch '{
        "Changes": [{
          "Action": "CREATE",
          "ResourceRecordSet": {
            "Name": "go.'$DOMAIN'",
            "Type": "A",
            "AliasTarget": {
              "HostedZoneId": "'$(aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN --query 'LoadBalancers[0].CanonicalHostedZoneId' --output text)'",
              "DNSName": "'$ALB_DNS'",
              "EvaluateTargetHealth": false
            }
          }
        }]
      }'
    
    Route 53 Alias records are preferred over CNAME for root domains and provide better performance (no additional DNS lookup).

    SSL/TLS Security Policies

    The default policy ELBSecurityPolicy-TLS-1-2-2017-01 supports:
    • TLS 1.2 and TLS 1.3
    • Modern cipher suites
    • Perfect Forward Secrecy (PFS)

    Available Policies

    List available SSL policies:
    aws elbv2 describe-ssl-policies \
      --query 'SslPolicies[].Name' \
      --output table \
      --region $AWS_REGION
    

    Stricter Policy (TLS 1.3 Only)

    For maximum security:
    aws elbv2 modify-listener \
      --listener-arn $LISTENER_ARN \
      --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
      --region $AWS_REGION
    
    Stricter policies may not work with older browsers. Test thoroughly before deploying to production.

    Test HTTPS Configuration

    1
    Test Certificate
    2
    Verify the certificate is installed correctly:
    3
    openssl s_client -connect $DOMAIN:443 -servername $DOMAIN < /dev/null
    
    4
    Look for:
    5
    Subject: CN=example.com
    Issuer: CN=Amazon, OU=Server CA 1B, O=Amazon, C=US
    Verify return code: 0 (ok)
    
    6
    Test HTTP to HTTPS Redirect
    7
    curl -I http://$DOMAIN
    
    8
    Expected response:
    9
    HTTP/1.1 301 Moved Permanently
    Location: https://example.com/
    
    10
    Test Application Access
    11
    # Test frontend
    curl -I https://$DOMAIN
    
    # Test backend API
    curl https://api.$DOMAIN/api/stats
    
    # Test short URL
    curl -I https://go.$DOMAIN/abc1234
    
    12
    SSL Labs Test
    13
    Run a comprehensive SSL test:
    14
    https://www.ssllabs.com/ssltest/analyze.html?d=example.com
    
    15
    Target grade: A or A+

    Update Application Configuration

    Update environment variables in your ECS task definitions:

    Backend Task Definition

    Update these values in infrastructure/task-def-backend.json:
    {
      "environment": [
        {
          "name": "APP_BASE_URL",
          "value": "https://go.example.com"  // Short URL domain
        },
        {
          "name": "CORS_ALLOWED_ORIGINS",
          "value": "https://example.com,https://www.example.com"  // Frontend domains
        }
      ]
    }
    
    Register the updated task definition and update the service:
    aws ecs register-task-definition \
      --cli-input-json file://infrastructure/task-def-backend.json \
      --region $AWS_REGION
    
    aws ecs update-service \
      --cluster adma-cluster \
      --service adma-backend \
      --task-definition adma-backend:2 \
      --force-new-deployment \
      --region $AWS_REGION
    

    Frontend Rebuild

    Rebuild and push the frontend with the HTTPS API URL:
    cd frontend
    
    docker build \
      --build-arg VITE_API_BASE_URL=https://api.$DOMAIN \
      -t $ECR_BASE/adma/frontend:latest \
      .
    
    docker push $ECR_BASE/adma/frontend:latest
    
    # Force ECS to pull new image
    aws ecs update-service \
      --cluster adma-cluster \
      --service adma-frontend \
      --force-new-deployment \
      --region $AWS_REGION
    

    Certificate Renewal

    ACM certificates automatically renew before expiration:
    • Renewal attempt: 60 days before expiration
    • DNS validation: Must remain valid for automatic renewal
    • No action required if DNS validation records exist

    Monitor Certificate Expiration

    Set up a CloudWatch alarm:
    aws cloudwatch put-metric-alarm \
      --alarm-name adma-cert-expiring \
      --alarm-description "Alert when ACM certificate is expiring" \
      --metric-name DaysToExpiry \
      --namespace AWS/CertificateManager \
      --statistic Minimum \
      --period 86400 \
      --evaluation-periods 1 \
      --threshold 30 \
      --comparison-operator LessThanThreshold \
      --dimensions Name=CertificateArn,Value=$CERT_ARN \
      --region $AWS_REGION
    

    Troubleshooting

    Certificate Stuck in PENDING_VALIDATION

    Cause: DNS validation record not found Solution:
    1. Verify the CNAME record is created correctly
    2. Check DNS propagation: dig _abc123.example.com CNAME
    3. Ensure you used the exact name and value from ACM

    NET::ERR_CERT_COMMON_NAME_INVALID

    Cause: Certificate doesn’t cover the domain you’re accessing Solution: Request a new certificate with the correct domain/SANs

    Mixed Content Warnings

    Cause: Frontend loading HTTP resources over HTTPS Solution: Ensure all resources (scripts, images) use HTTPS URLs or relative paths

    CORS Errors After HTTPS

    Cause: CORS configuration still allows HTTP origins Solution: Update CORS_ALLOWED_ORIGINS to use HTTPS URLs

    Security Best Practices

    • ✅ Use TLS 1.2 or higher (disable TLS 1.0/1.1)
    • ✅ Enable HTTP Strict Transport Security (HSTS)
    • ✅ Use wildcard certificates sparingly
    • ✅ Rotate certificates annually (ACM does this automatically)
    • ✅ Monitor certificate expiration
    • ✅ Test SSL configuration with SSL Labs

    Next Steps

    With HTTPS configured:
    1. Enable security headers (HSTS, CSP, X-Frame-Options)
    2. Set up CloudWatch monitoring and alarms
    3. Configure auto-scaling for ECS services
    4. Implement CI/CD for automated deployments
    5. Create production database backups

    Build docs developers (and LLMs) love