DEV Community

Mohamed Wasim for itTrident Software Services

Posted on • Edited on

Deploy Fider as a Private App on AWS with CloudFront VPC Origin

An important consideration when architecting a solution on AWS is minimizing the potential attack surface and limiting the opportunities for an attacker to target your application through a layered approach. In today's cloud landscape, ensuring privacy and security is critical during deployment. Many applications are exposed to the public internet, which inherently increases the risk of attacks. To address this, AWS has introduced CloudFront Virtual Private Cloud (VPC) Origins, a new feature that enables users to deliver content securely from applications hosted within a private VPC subnet, keeping origin servers hidden from direct internet exposure.

In this guide, I’ll explain:

1. What is CloudFront VPC Origin

Traditionally, when using CloudFront with an origin resource (such as an ALB or EC2 instance), the origin needs to be in a public subnet with a public IP address, implementing Access Control Lists (ACLs) and other controls to restrict access effectively. Users needed to invest ongoing effort to implement and maintain these solutions, resulting in undifferentiated heavy lifting.

However, With CloudFront VPC Origins, users can host their applications in a private VPC, without requiring any direct route to the internet and make sure CloudFront is the only entry point to their applications. When CloudFront VPC Origin is set up as an origin for the CloudFront distribution, traffic stays on the high-throughput AWS Backbone network all the way to your AWS origin, making sure of optimized performance and low latency.

This is designed to prevent end users from discovering or bypassing CloudFront to access web applications directly. As a result, the origin servers remain hidden on the internet (Obfuscating AWS resources), significantly reducing the attack surface (attack surface reduction) and enhancing the overall security posture. At the same time, users continue to benefit from the CloudFront global scale and high-performance capabilities.

2. Why you should deploy Fider as a private application

Alright, here’s the deal: With this setup, even your ALB can be internal, and it'll route traffic straight to your ECS instances in those private subnets. You heard that right, no public access. This keeps your backend services completely hidden from the internet, which is exactly what you want for security.

Now, with CloudFront VPC origin, you can stick that ALB in a private subnet where nobody can get to it directly. CloudFront is the only way in, so you can rest easy knowing the attack surface is way smaller. Plus, that ALB? The DNS name only resolves to private IPs, so no internet users can mess with it.

And if you’re really serious about security, you can slap on Origin Custom Headers to make sure only your CloudFront distribution is allowed to talk to the ALB. It’s like building a fortress around your infrastructure. But wait, it gets better, those custom headers can be dynamically rotated using AWS Secrets Manager and a Lambda function automation, keeping everything secure without manual intervention.

3. Create an Internal Application Load Balancer

Login to management console and navigate to EC2, under Load Balancing section choose Load Balancers,

Image description

Click on create load balancer, and choose Application Load Balancer

Image description

Under Basic Configuration choose Internal as scheme,

Image description

Under Network Mapping choose the VPC you created or refer this guide to create a VPC, creating a single public subnet should suffice this time (for placing NAT gateway).

Image description

Choose to launch the Application Load Balancer in at least 2 Azs, in private subnets.

Image description

Choose the Security group for your internal ALB and create an HTTPS listener,

Image description

Navigate to the VPC console and click on the Managed prefix lists, choose pl-b6a144df and map this to your ALB's inbound SG rules(HTTPS), by doing so you are only letting the CloudFront's IP ranges to communicate with your ALB. This is the recommended approach according to AWS Best Practices for DDoS Resiliency.

Image description

Under Secure listener settings choose the certificate you created from ACM, if not you can refer this guide and forward to Step 3.

Image description

Once you are done with requesting the public certificate in ACM, your certificate will appear in the drop-down select it, scroll down, and click on create load balancer.

Image description

The internal ALB is now created with the traffic originating from the CloudFront IP ranges.

4. Create CloudFront VPC origin

Now that you are done with the internal ALB creation, Navigate to the CloudFront service console and choose VPC origins from the navigation pane.

Image description

Click on VPC origins and create a VPC origin,

Image description

Provide a Name and choose internal ALB's ARN from the Origin ARN drop-down,

Image description

Choose the protocol to HTTPS only and click on create VPC origin.

Image description

The VPC origin creation is now complete.

5. Create a CloudFront Distribution

Navigate to the CloudFront service console and click on Create a CloudFront distribution,

Image description

Under the Origin section, select the VPC origin you created. Enable Origin Shield for an additional layer of caching, and don't forget to add the custom header, name it x-origin-verify.

Image description

Under Default cache behaviour choose the following settings, under Allowed HTTP methods choose the third option, if you want to know why then check out this guide for reason.

Image description

Under Cache key and origin requests choose Cache policy and origin request policy (recommended). Choose the default cache policy and origin request policy. Under Response headers policy choose CORS-with-preflight-and-SecurityHeadersPolicy, it is an AWS-managed response header policy. Use this policy to specify security-related HTTP response headers that CloudFront adds to HTTP responses that it sends to viewers.

Image description

The AWS-managed policy provides the following security headers, it ultimately narrows down to your use case, you can also create a Custom policy and attach it under Response headers policy.

Image description

Enable Web Application Firewall for your CloudFront distribution, toggle on use monitor mode you can later disable it once you get know how much of your requests are being blocked by this WAF configuration(False positives and red herrings).

Check SQL protections and Rate limiting(DDoS protection) and take a note of price estimation.

Image description

Add the CNAME through which you would like to access your application publicly and choose the custom SSL certificate for your CNAME.

Image description

Configure standard logging for your CloudFront distribution to monitor the performance and usage metrics.

Image description

The CloudFront distribution creation is now complete.

Image description

Navigate to the WAF & Shield console, click on Web ACLs and choose Global(CloudFront) in the region drop-down. Click on the Web ACL and make a note of the rules and WCUs.

6. Create Secrets manager secret and Lambda function

  • Choose secret type:

Navigate to the Secrets manager service console, and click on Store a new secret.

Image description

Under Secret type choose Other type of secret, and provide HEADERVALUE as a Key and paste the same header value you used in the CloudFront distribution. Click on next.

Image description

  • Configure secret:

Choose the secret name and click on next, take a note of Resource permissions step, we will come back to this step once we are done creating the lambda function.

Image description

  • Review:

Skip to the review section leaving behind the configure rotation step, as we will configure that once we create and configure the lambda function.

Image description

The secret creation is now complete.

Image description

  • Create a rotator lambda function:

Navigate to the lambda console and click on Create function,

Image description

Choose Author from scratch and choose Python 3.9 as runtime with architecture as x86_64. Under Execution role choose create a new role, this creates a new service role for the function.

Image description

Under Additional Configurations enable tags and VPC, i am placing lambda function inside VPC to enable VPC flow logs to monitor network traffic, which is critical for auditing and incident response. Before moving onto choosing the security group create a new one in the EC2 console and then revert back here.

Image description

Since this rotator Lambda function will be triggered by Secrets Manager, it won't be needing any inbound rules, as it's event-driven. It still needs one outbound rule, to reach out to the Secrets manager endpoint via HTTPS protocol, as the traffic (for HTTPS) will be routed through public internet via a NAT-gateway.

Image description

Now, reference the created SG under the lambda's configuration parameters drop-down and click on Create function.

Image description

The rotator-lambda function is now created.

  • Configuration parameters - rotator lambda function:

Use the python code https://gist.github.com/WasimTTY/ff092402d09c56f6496b359036e42aa0 from this gist to deploy the code onto the lambda function.

This code was modified to work with the CloudFront WAF configuration.

Image description

Once pasted, click on deploy to deploy the code to the lambda function. Additionally, this code does come with an external dependency, so create a lambda layer and attach it to the rotator-lambda function.

Image description

You can find the artifact in this repository: https://github.com/aws-samples/amazon-cloudfront-waf-secretsmanager/tree/master/artifacts

Image description

Now, under the Configuration tab choose the Timeout for the rotator-lambda function, i have set it to 5 mins 3 sec with the memory and the Ephemeral storage left to defaults.

Image description

Take a note of the Existing role attached to the lambda function. we will be needing it attach the policies to let the lambda function interact with the CloudFront distribution and WAF for rotating the secret-custom-header.

Navigate to the IAM console and under Roles tab search for the service role created by the rotator-lambda function.

Image description

Create an inline policy for accessing the CloudFront distribution, I have only provided a very limited permissions. This inline policy grants the rotator Lambda function the minimal permissions required to make API calls to the CloudFront distribution for modifying the Secret-Custom-Header.

  {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "cloudfront:GetDistribution",
                "cloudfront:UpdateDistribution",
                "cloudfront:GetDistributionConfig"
            ],
            "Resource": "arn:aws:cloudfront::<AWS ACCOUNT ID>:distribution/E3R3ZKP77W4214",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalArn": "arn:aws:iam::<AWS ACCOUNT ID>:role/service-role/header-rotate-function-role-tinh6qnu"
                }
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

I have done the same for Secrets manager, CloudFront WAF and lambda-VPC policies.

  {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret",
                "secretsmanager:PutSecretValue",
                "secretsmanager:UpdateSecretVersionStage"
            ],
            "Resource": "arn:aws:secretsmanager:us-east-2:<AWS ACCOUNT ID>:secret:Custom-Header-lQWRrQ",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalArn": "arn:aws:iam::<AWS ACCOUNT ID>:role/service-role/header-rotate-function-role-tinh6qnu"
                }
            }
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "secretsmanager:GetRandomPassword",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalArn": "arn:aws:iam::<AWS ACCOUNT ID>:role/service-role/header-rotate-function-role-tinh6qnu"
                }
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode
  {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateNetworkInterface",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DeleteNetworkInterface",
                "ec2:UnassignPrivateIpAddresses",
                "ec2:DescribeSubnets",
                "ec2:AssignPrivateIpAddresses"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalArn": "arn:aws:iam::<AWS ACCOUNT ID>:role/service-role/header-rotate-function-role-tinh6qnu"
                }
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode
  {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "wafv2:UpdateWebACL",
                "wafv2:GetWebACL"
            ],
            "Resource": [
                "arn:aws:wafv2:*:<AWS ACCOUNT ID>:*/regexpatternset/*/*",
                "arn:aws:wafv2:*:<AWS ACCOUNT ID>:*/managedruleset/*/*",
                "arn:aws:wafv2:*:<AWS ACCOUNT ID>:*/rulegroup/*/*",
                "arn:aws:wafv2:us-east-1:<AWS ACCOUNT ID>:global/webacl/CreatedByCloudFront-4ffb490d-8e92-419b-a38b-017361fd80aa/a2512452-7f08-4591-bf1a-fec1923ac75a",
                "arn:aws:wafv2:*:<AWS ACCOUNT ID>:*/ipset/*/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalArn": "arn:aws:iam::<AWS ACCOUNT ID>:role/service-role/header-rotate-function-role-tinh6qnu"
                }
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

The IAM policies crafted above are identity-based policies scoped down to a specific IAM role using a PrincipalArn condition, enforcing least privilege.

The rotator-lambda function also needs a resource-based policy in order for the Secrets manager to invoke the function. I have used AWS CLI to add this policy to the lambda function.

  aws lambda add-permission \
  --function-name secret-header-rotator-function \
  --statement-id AllowSecretsManagerInvoke \
  --action lambda:InvokeFunction \
  --principal secretsmanager.amazonaws.com \
  --source-account <AWS ACCOUNT ID> \
  --source-arn arn:aws:secretsmanager:us-east-2:<AWS ACCOUNT ID>:secret:Custom-Header-lQWRrQ

Enter fullscreen mode Exit fullscreen mode

The resultant policy will look like this,

  {
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "AllowSecretsManagerInvoke",
      "Effect": "Allow",
      "Principal": {
        "Service": "secretsmanager.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-east-2:<AWS ACCOUNT ID>:function:secret-header-rotator-function",
      "Condition": {
        "StringEquals": {
          "AWS:SourceAccount": "<AWS ACCOUNT ID>"
         },
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:secretsmanager:us-east-2:<AWS ACCOUNT ID>:secret:Custom-Header-lQWRrQ"
        }
      }
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

The resource-based policy above follows the Zero Trust principle by minimizing trust ensuring access is explicitly verified through conditions like Source Account and Source ARN before granting permissions.

  • Configure secrets manager - resource policy:

Now, navigate to the Secrets manager service console and under the Resource permissions paste the resource-based policy for invoking the rotator-lambda function.

  {
  "Version" : "2012-10-17",
  "Statement" : [ {
    "Sid" : "AllowLambdaAccess",
    "Effect" : "Allow",
    "Principal" : {
      "AWS" : "arn:aws:iam::<AWS ACCOUNT ID>:role/service-role/header-rotate-function-role-tinh6qnu"
    },
    "Action" : [ "secretsmanager:GetSecretValue", "secretsmanager:PutSecretValue", "secretsmanager:UpdateSecretVersionStage", "secretsmanager:DescribeSecret" ],
    "Resource" : "arn:aws:secretsmanager:us-east-2:<AWS ACCOUNT ID>:secret:Custom-Header-lQWRrQ",
    "Condition" : {
      "StringEquals" : {
        "aws:ResourceTag/Environment" : "production",
        "aws:SourceAccount" : "<AWS ACCOUNT ID>",
        "aws:SourceArn" : "arn:aws:lambda:us-east-2:<AWS ACCOUNT ID>:function:secret-header-rotator-function"
      }
    }
  } ]
}

Enter fullscreen mode Exit fullscreen mode

This policy also aligns with the Zero Trust principle, Even though the request is coming from the rotator-lambda function (which could be considered a trusted service), the policy does not allow access by default. It enforces strict conditions including ABAC (Attribute Based Access Control) using resource tags to validate the request’s origin and ensure the caller is explicitly authorized.

Taking this approach tightens security by enforcing stringent, context-aware access controls.

7. Create a Custom Header rule in AWS WAF

Navigate to the WAF & Shield service console and look-up for Web ACLs created under the Global(CloudFront).

Image description

Create a new rule named XOriginVerify, and configure the same secret-custom-header value that was set in the CloudFront distribution and the Secrets manager.

Image description

The rotator-lambda function looks Explicitly for this rule name as it is hard-coded in the python code. You can modify it according to your use case.

Once the rotator-lambda function updates the secret-custom-header in both the CloudFront distribution and in WAF, it tests the secret by pinging your domain-name, in this case the request has to propagate through the CloudFront, and WAF will let the request through only if that header matches, it should result in 200 OK. You have to check CloudWatch logs for this.

8. Why Implement Zero Trust Principle

Before you slap on Zero Trust policies like they’re the new fad, you gotta do threat modeling first. It’s like doing a security audit before you buy a new lock. You gotta figure out:

  • What you’re protecting ?
  • Who’s trying to break in ?
  • How they might do it ?

That last part? That’s basically recon and attackers do it all the time. They scan your public IPs, poke at your APIs, look for leaky S3 buckets or overly permissive roles. So when you threat model, you're doing a defensive version of recon: identifying what an attacker might see and how they'd try to exploit it.

Threat modeling helps you spot risks like this, Once you know that, you can set up your Zero Trust stuff, like least privilege access, network segmentation, and strong authentication all tailored to the actual threats, not just random paranoia.

Although this Lambda function is small, its capabilities are significant. In cloud environments, Lambda can be a double-edged sword and powerful for automation, but dangerous if misconfigured.

This strict posture isn't just theoretical. Tools like AWSRedirector demonstrates how serverless components can be exploited in real-world attacks . That's why we lock down even the smallest moving parts.

Without threat modeling, you’d be like, ‘Oh, I’ll just block everything!’ but that’s like building a fortress with no doors. You gotta know who’s allowed in, and under what conditions. That’s how Zero Trust works it’s not about ‘trust no one’ for the sake of it, it’s about verifying every request based on real risks. You should understand the balance between security and usability.

If you’re curious about how AWS approaches Zero Trust in detail, you can check out their Zero Trust Security Model and AWS Zero Trust Architecture principles.

9. Spin up ECS and RDS

You can refer to this guide for spinning up the ECS and RDS infrastructure.

10. Monitor CloudFront and WAF logs

Monitoring CloudFront and AWS WAF logs is a crucial part of incident response in any secure AWS setup, especially when you're running a public-facing app.

In a Zero Trust setup, security doesn’t stop at blocking threats, you need visibility into what’s happening at your edge and app layers. That’s where CloudFront and AWS WAF logs come in.

In the AWS management console under the region drop-down choose us-east-1, this is where CloudFront delivers logs to in the CloudWatch log-group.

Image description

Each log contains information such as the time the request was received, the processing time, request paths,client-IP and server responses. You can use these access logs to analyze response times and to troubleshoot issues.

Image description

When you're absolutely done observing the false positives for WAF rules, navigate to the WAF console and adjust the actions for the rules according to your use case. You can choose whether to set actions such as Allow, Block, Captcha, or Count based on your specific needs. It ultimately narrows down to your requirements and the level of protection you wish to enforce.

It’s important to note that when you set a rule group action to 'Count,' it will override any Allow, Block, or Captcha actions that were originally specified for the rules within that group. As a result, all rules in the group will only count matching requests, regardless of their original action.

By default, the action is set to 'Count' during testing and monitoring of the rule group’s behavior, which allows you to observe how the rules perform before deploying them to production.

AWS WAF now supports sending logs to CloudWatch logs, Hover over to the Logging and metrics tab and click on Enable drop-down and choose Logging destination.

Image description

Choose the CloudWatch logs log group as destination and click on save,

Image description

Logging is now enabled for AWS WAF.

Image description

Hover over to the Sampled requests tab to help you analyze how your AWS WAF rules are evaluating traffic. Each sampled request shows details like the IP address, headers, URI, country, and rule evaluation outcome. Sampled requests are free.

Image description

Image description

In addition to this, you can also use the default queries provided by WAF to query the number of requests that come from an IP address. Under the Cloudwatch logs insight tab in the WAF console.

Image description

11. Caveats to consider

  • VPC Endpoints:

You can still choose to go with VPC interface endpoints (Powered by AWS PrivateLink) if you haven't integrated external OAuth providers (like Google, GitHub, etc.) because these services are not available via AWS PrivateLink.

When you configure OAuth providers with Fider, the authentication requests go to the OAuth provider's endpoints over the internet. This involves external APIs that cannot be routed through VPC Interface Endpoints directly, since these OAuth services are external to your AWS infrastructure and not hosted within your VPC or private AWS environment.

It will also cost you more than the NAT gateway, you can checkout this post from the official AWS forum for even more clarification. Only choose this method if you consider security as paramount.

  • Potential Attack Vector: CloudFront and ALB

If you choose to ignore Secret-Custom-Header validation & rotation in your setup, relying solely on CloudFront IP ranges in the ALB security group introduces a potential attack vector.

Your ALB cannot differentiate between requests from your CloudFront distribution and requests from any other CloudFront customer. it is absolutely possible for an attacker to bypass CloudFront and access your internal ALB. Since all CloudFront traffic uses the same IP address ranges, an attacker could spin up their own CloudFront distribution and point it at your ALB, bypassing IP-based restrictions alone.

  • Staged Header Rotation in WAF

Because the custom header is only validated by your XOriginVerify rule in AWS WAF at the internal ALB, the Lambda function stages the new header value alongside the existing one in the WAF rule. This ensures that during rotation, origin requests bearing either the old or new header are accepted, preventing 403 Forbidden errors. Once you’re confident the new header is in use, the old value can be safely removed from the WAF rule (This is a manual step).

12. Final thoughts

Let’s be real no system is made of adamantium. Given enough time, resources, or misconfigurations, even the most locked-down architecture can be poked at. But this setup doesn’t aim for invincibility, it aims to minimize exposure, limit blast radius, and make exploitation significantly harder and riskier.

This architecture trades convenience for control, reducing your attack surface, slowing down adversaries, and giving you time and telemetry to respond.

Quadratic AI

Quadratic AI – The Spreadsheet with AI, Code, and Connections

  • AI-Powered Insights: Ask questions in plain English and get instant visualizations
  • Multi-Language Support: Seamlessly switch between Python, SQL, and JavaScript in one workspace
  • Zero Setup Required: Connect to databases or drag-and-drop files straight from your browser
  • Live Collaboration: Work together in real-time, no matter where your team is located
  • Beyond Formulas: Tackle complex analysis that traditional spreadsheets can't handle

Get started for free.

Watch The Demo 📊✨

Top comments (0)