Skip to content
Ashish.
All posts
Diagram illustrating the trust chain between API Gateway, Lambda, and DynamoDB with IAM policy boundaries.

Serverless Security on AWS: Lambda, API Gateway, and DynamoDB

An examination of serverless security on AWS focusing on securing Lambda functions, API Gateway authorization, and DynamoDB access policies.

By Ashish Srivastava

The common misconception in serverless architecture is that security is defined by the perimeter of the network. In a serverless model, the network perimeter dissolves; every request originates from a managed AWS service IP range, and the function itself is ephemeral. Security shifts from "who can reach the server" to "what can this specific identity do at this specific moment." This article dissects the mechanism of trust between AWS Lambda, API Gateway, and DynamoDB, moving beyond generic "best practices" to explain how identity propagation and policy evaluation actually work under the hood.

The Execution Role: Identity, Not Privilege

When you deploy a Lambda function, you attach an execution role. A developer often assumes this role grants the function permission to do things. It does not. The execution role is an Identity, not a set of privileges. When the function runs, AWS assumes the role, creating a temporary set of credentials. The actual permission check happens at the API call level, not the code level.

Consider a scenario where a Lambda function needs to write to a DynamoDB table. If you grant the execution role dynamodb:* on *, the function has no mechanism to distinguish between writing to users-table and users-backup-table. The mechanism of defense here is the principle of least privilege enforced at the resource level. You must attach a policy that explicitly lists the table ARN.

However, the most critical mechanism is the evaluation context. AWS evaluates policies using the aws:PrincipalArn of the assumed role. If your function code attempts to assume another role (a common pattern for cross-service access), the original execution role's permissions are irrelevant to the new role's actions. The new role becomes the identity.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "dynamodb:GetItem",
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/users"
    }
  ]
}

If an attacker compromises the function code, they cannot use it to access other tables unless those tables are explicitly included in the resource ARN. The mechanism relies on the Resource field in the IAM policy being as granular as possible.

Technical diagram showing an AWS Lambda function with an attached IAM execution role. The diagram should illustrate the flow of temporary credentials generated by AWS STS, the evaluation context including aws : PrincipalArn, and the distinction between the identity of the role…

API Gateway Authorization: The Trust Chain

API Gateway acts as the entry point. The mechanism of authorization determines whether a request reaches the Lambda function at all. There are two primary paths: Cognito User Pools (managed) or a custom Lambda Authorizer.

In a Lambda Authorizer flow, the API Gateway receives the request, extracts the token (usually a JWT), and invokes your authorizer function. This function returns a policy document. This policy document is then used to generate a temporary IAM session for the next step: invoking the actual Lambda function.

Here is the critical mechanism: the API Gateway invokes the downstream Lambda function using its own service role credentials, not the user's. The downstream function runs with its own execution role permissions, not the user's. The user's identity is passed only as data in the requestContext object inside the Lambda function, specifically within the authorizer claims. These claims are just data; they do not carry the permissions of the user who made the API call. The permissions are entirely derived from the execution role of the API Gateway Lambda Authorizer or the API Gateway service role.

If you rely solely on the JWT claims to authorize access in your main function logic (e.g., if (user.role === 'admin')), you are vulnerable to replay attacks or token forgery if the signature verification is flawed. The secure mechanism is to have the API Gateway enforce the policy before the request ever hits the main Lambda.

// Lambda Authorizer Logic
exports.handler = async (event) => {
  const token = event.headers.authorization;
  // Verify token...
  
  return {
    principalId: 'user123',
    policyDocument: {
      Version: '2012-10-17',
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: 'Allow',
        Resource: event.methodArn
      }]
    }
  };
};

This policy allows the API Gateway to invoke the downstream function. The separation ensures that even if the function code is buggy, the API Gateway prevents unauthorized invocation. The user's identity is only available as data in event.requestContext.authorizer.

DynamoDB Access: The Policy Intersection

DynamoDB security is often misunderstood because it supports two distinct types of policies: IAM policies attached to users/roles, and resource-based policies attached to the table itself. The mechanism of access control is a logical AND. A request is allowed only if the IAM policy allows it AND the resource policy allows it.

In a serverless context, the Lambda function's execution role is the caller. The IAM policy on that role must allow dynamodb:Query or dynamodb:GetItem. However, the resource policy on the table must also allow the specific role ARN to perform the action.

The most dangerous vulnerability here is the "wildcard" resource. If you grant Resource: "*" in the IAM policy, the function can access any table in the account. But if the table's resource policy is restrictive, it blocks the access. Conversely, if the table policy is Resource: "*" (which is rare but possible), the IAM policy becomes the sole gatekeeper.

A more advanced mechanism is the dynamodb:Keys condition key. This allows you to restrict access to specific partition keys or sort keys. However, dynamodb:Keys matches against the actual keys provided in the request parameters, not IAM variables like ${aws:username}. To implement row-level security, you should use dynamodb:LeadingKeys for prefix matching or enforce the logic in your application code using the UserId from the API Gateway requestContext.

To prevent data exfiltration, you must avoid dynamodb:Scan and dynamodb:Query with wildcards. The mechanism of defense is to force the application to use GetItem with a known partition key. If you must allow queries, the IAM policy should restrict the Limit parameter or the ProjectionExpression.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReadOwnItems",
      "Effect": "Allow",
      "Action": "dynamodb:GetItem",
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/users/item/*",
      "Condition": {
        "StringEquals": {
          "dynamodb:Keys": "${requestParams.ItemKey}"
        }
      }
    }
  ]
}

The final layer of defense is network isolation. By default, Lambda functions run in a public subnet unless you specify a VPC. If a function is public, it is reachable via the public internet through API Gateway. If you move the function into a VPC, you gain control over the network path.

The mechanism here is the VPC Endpoint (Interface or Gateway). If you create a VPC Endpoint for DynamoDB, traffic from your Lambda function to DynamoDB stays within the AWS network backbone. It never traverses the public internet. This eliminates the risk of DNS hijacking or man-in-the-middle attacks on the data path.

For API Gateway, you can enable VPC Links. This allows you to route traffic from the API Gateway directly to your Lambda function inside the VPC without exposing the function to the public internet. The API Gateway acts as the ingress, but the function itself is never assigned a public IP.

However, there is a trade-off. Placing Lambda in a VPC requires a NAT Gateway if the function needs to access the internet (e.g., downloading a library). This adds cost and complexity. The decision to use VPC isolation should be based on the sensitivity of the data. For public-facing APIs, the API Gateway WAF (Web Application Firewall) is often the primary defense, while VPC isolation is reserved for internal microservices or sensitive data processing.

Network architecture diagram showing an AWS Lambda function deployed inside a VPC. Illustrate the VPC Endpoint (Interface Endpoint) connecting Lambda to DynamoDB privately, bypassing the public internet. Show the VPC Link connecting API Gateway to the private Lambda. Use arrow…

Conclusion

Securing serverless architectures on AWS is not about configuring a firewall; it is about orchestrating the flow of identity and permissions. The Lambda execution role defines what the function can do, the API Gateway authorizer defines who can ask the function to do it, and the DynamoDB policies define what data the function can touch.

The mechanism of trust is a chain: the user authenticates at the gateway, the gateway validates the token and invokes the function, the function assumes its role, and the role checks the resource policies. Breaking any link in this chain—by granting excessive IAM permissions, relying solely on client-side validation, or exposing the function to the public internet without a WAF—collapses the entire security model. Always evaluate your policies against the specific resource ARN and the Condition keys available to ensure that even if the code is compromised, the attacker cannot move laterally or exfiltrate data. This holistic approach is the foundation of robust serverless IAM.

FAQ

How does IAM evaluate policies? IAM evaluates policies by checking the aws:PrincipalArn of the identity making the request against the Principal in the policy statement, then verifying if the requested Action and Resource match the Effect (Allow/Deny). If a request matches both the IAM policy and the resource policy, it is allowed; otherwise, it is denied.

Can I use Cognito with custom authorizers? Yes, you can use Cognito User Pools for authentication, but you can still configure a Lambda Authorizer to add custom logic or map Cognito groups to specific API permissions. The Cognito identity provides the principalId and claims, which the authorizer function can then use to generate the IAM policy for the downstream Lambda invocation.

What is the risk of wildcard resources in DynamoDB? Using Resource: "*" in an IAM policy allows the identity to access every DynamoDB table in the account. If the table's resource policy is not restrictive, this creates a massive blast radius where a compromised function can read or write to any table. Even if the table policy is restrictive, relying on it as the sole defense is risky; the IAM policy should always be the first line of defense with the most restrictive scope possible.

Practical Takeaways

  1. Identity is Separate from Privilege: An IAM role provides an identity for the function, but the specific permissions are strictly defined by the attached policy. Never assume a role grants access to everything.
  2. The Gateway is the Gatekeeper: API Gateway should enforce authorization policies before the request reaches your main Lambda function. Do not rely on application logic within the function to validate user access.
  3. Defense in Depth for Data: Combine IAM policies, resource-based policies, and condition keys to secure DynamoDB. Never rely on a single policy layer for row-level security.

Common Pitfalls

  • Overly Broad IAM Roles: Granting AdministratorAccess or dynamodb:* to a Lambda execution role to make development easier, leaving the production environment vulnerable to lateral movement.
  • Client-Side Validation Reliance: Assuming that hiding API endpoints or validating tokens in the frontend code is sufficient, ignoring the need for backend authorization checks.
  • Ignoring Resource Policies: Focusing solely on IAM policies for the Lambda role and forgetting that DynamoDB resource policies are also required to restrict access from specific principals.

Related posts