DEV Community

Cover image for Implementing Secure Access Control using AWS WAF with IP Address and BASIC Authentication
Shinji NAKAMATSU
Shinji NAKAMATSU Subscriber

Posted on

Implementing Secure Access Control using AWS WAF with IP Address and BASIC Authentication

Photo by Damon Lam on Unsplash


Introduction

AWS WAF is a Firewall service that can be attached to CloudFront or ALB. By defining a Web ACL (Access Control List), you can block or allow access based on specific conditions.

It is common to want to restrict who or where can access your service while developing a new service to be exposed on the internet.

In this article, we share how to combine AWS WAF for allowing access based on specific IP addresses (CIDR) and BASIC authentication.

Requirements

  • Allow access if it matches the allowed IP addresses (CIDR)
  • Allow access if the request header contains correct BASIC authentication information
  • Block all other access

Overview of the Mechanism

Here's a summary of the mechanism:

Overview Diagram

  • Default to "allow access"
    • Assuming that when opening the service, just removing the rule would remove the access restriction
  • Block access if the following (AND) conditions are met:
    • Not an allowed IP address
    • Lacks BASIC authentication information
  • When blocking access, request BASIC authentication with a custom response (WWW-Authenticate header is returned)

Terraform Code

Here's the code snippet:

// modules/waf/variables.tf // Module parameters variable "allowed_ip_list" {} variable "basic_auth" { type = object({ user = string password = string }) } 
Enter fullscreen mode Exit fullscreen mode
// modules/waf/main.tf terraform { required_providers { aws = { source = "hashicorp/aws" configuration_aliases = [aws.virginia] } } } locals { // Pre-calculate the expected value for the Authorization header credential = base64encode("${var.basic_auth.user}:${var.basic_auth.password}") } resource "aws_wafv2_web_acl" "main" { provider = aws.virginia name = "example-web-acl" description = "Web ACL example" scope = "CLOUDFRONT" // Set the default action to allow default_action { allow {} } // BASIC Authentication rule { name = "basic-auth" priority = 20 action { block { // Set the action to block for the conditions mentioned later // Also, return a custom response requesting username and // password input for BASIC authentication custom_response { response_code = 401 response_header { name = "WWW-Authenticate" value = "Basic realm=\"Secure Area\"" } } } } // Conditions for Block (AND) : statement { and_statement { statement { // Condition 1 // The source IP address is not included in the allowed IP list not_statement { statement { ip_set_reference_statement { arn = aws_wafv2_ip_set.allowed_ips.arn } } } } statement { // Condition 2 // The Authorization header does not contain not_statement { statement { byte_match_statement { positional_constraint = "EXACTLY" search_string = "Basic ${local.credential}" field_to_match { single_header { name = "authorization" } } text_transformation { priority = 0 type = "NONE" } } } } } } } visibility_config { cloudwatch_metrics_enabled = true metric_name = "example-basic-auth" sampled_requests_enabled = true } } visibility_config { cloudwatch_metrics_enabled = true metric_name = "example-web-acl" sampled_requests_enabled = true } } // Set of allowed IP addresses for access resource "aws_wafv2_ip_set" "allowed_ips" { provider = aws.virginia name = "example-allowed-ips" description = "Authorized IP addresses" scope = "CLOUDFRONT" ip_address_version = "IPV4" addresses = var.allowed_ip_list } 
Enter fullscreen mode Exit fullscreen mode

IP Address-based Access Control Settings

To perform access control based on IP addresses in AWS WAF, you first need to prepare an IP set.

// Set of allowed IP addresses resource "aws_wafv2_ip_set" "allowed_ips" { provider = aws.virginia name = "example-allowed-ips" description = "Authorized IP addresses" scope = "CLOUDFRONT" ip_address_version = "IPV4" addresses = var.allowed_ip_list } 
Enter fullscreen mode Exit fullscreen mode

In the above, the list of IP addresses is stored in var.allowed_ip_list, and it's assumed that the module user will pass it as a parameter in the following manner:

module "waf" { src = "../modules/waf" allowed_ip_list = [ "xxx.xxx.xxx.xxx/32", "yyy.yyy.yyy.yyy/32", "zzz.zzz.zzz.zzz/32" ] ... } 
Enter fullscreen mode Exit fullscreen mode

BASIC Authentication Settings

In the code below, we pre-calculate the string expected to be stored in the BASIC authentication header.

locals { // Pre-calculate the expected value for the Authorization header credential = base64encode("${var.basic_auth.user}:${var.basic_auth.password}") } 
Enter fullscreen mode Exit fullscreen mode

In the following block, we reference that value to check whether the corresponding string is stored in the request's Authorization header.

 byte_match_statement { positional_constraint = "EXACTLY" search_string = "Basic ${local.credential}" field_to_match { single_header { name = "authorization" } } text_transformation { priority = 0 type = "NONE" } } 
Enter fullscreen mode Exit fullscreen mode

If the conditions do not match, the block below is set to return a custom response.

 action { block { // Set the action to block for the conditions mentioned later // Also, return a custom response requesting username and // password input for BASIC authentication custom_response { response_code = 401 response_header { name = "WWW-Authenticate" value = "Basic realm=\"Secure Area\"" } } } } 
Enter fullscreen mode Exit fullscreen mode

In the custom response, the WWW-Authenticate response header requests BASIC authentication from the source. Typically, when a browser receives this response, it displays a dialog prompting for BASIC authentication information (User/Password).

Reference: WWW-Authenticate - HTTP | MDN

Combining IP Address and BASIC Authentication

Combining the conditions mentioned earlier results in the following rule. (Details are omitted for clarity in structure)

rule { // If the following statement matches, block the access and return a custom response action { block { // Request BASIC authentication in the custom response } } statement { and_statement { // The source IP address is not in the allowed IP list statement { // Invert the condition here not_statement { statement { // Does the IP address match the allowed IP list? } } } // BASIC authentication information is not included in the Authorization header statement { // Invert the condition here not_statement { statement { // Is BASIC authentication information included in the Authorization header? } } } } } } 
Enter fullscreen mode Exit fullscreen mode

Note that, as the negation (not_statement) cannot be described directly under the rule block or and_statement, it needs to be re-wrapped with the statement block, making the structure somewhat difficult to understand.

In summary, this results in the code mentioned earlier.

Conclusion

We introduced a method to allow access to specific users using AWS WAF by combining IP address-based strict access control and looser access control through BASIC authentication, which can flexibly accommodate cases where temporary access is needed.

One point to note is that if you apply this to APIs that require authentication methods other than BASIC authentication (e.g., OAuth), some modification on the client-side using the API may be needed, bringing infrastructural concerns into the application layer. It's also viable to exclude such APIs from this rule if they already have authentication in place, depending on the risk assessment.

I hope this article helps someone out there.

References

Top comments (0)