Project Zero: Cloud Lab (Part 3)

case study

Serverless (Lambda)

last edited 2026-09-02

I built a serverless AWS threat-intelligence pipeline using Lambda, S3, IAM, VPC networking, and PostgreSQL. I configured the supporting Lambda environment and permissions, deployed a public Lambda function to collect threat-intelligence data and store it as JSON in S3, and used S3 events to trigger a private Lambda function that ingested the data into PostgreSQL. I then deployed the Threat Intelligence Dashboard and verified the complete workflow from public feed collection through database ingestion to displaying the data in the application.

#Prepare Lambda Environment

1.Configure Lambda S3 Permissions

I configured the Lambda execution role with scoped S3 permissions allowing the threat-intelligence functions to read and write objects in the dedicated threat-intelligence feed bucket.

Configured the Lambda execution role with  s3:GetObject  and  s3:PutObject  permissions for the threat-intelligence feed bucket.
Configured the Lambda execution role with s3:GetObject and s3:PutObject permissions for the threat-intelligence feed bucket.

2.Create Shared Lambda Layer

I created a shared Lambda layer containing the Python dependencies required by the threat-intelligence functions. Packaging the dependencies as a reusable layer keeps the individual Lambda deployment packages smaller and allows multiple functions to use the same libraries.

Uploaded the packaged  threat-intel-lambda-layer.zip  and configured the layer for the Python 3.12 runtime.
Uploaded the packaged threat-intel-lambda-layer.zip and configured the layer for the Python 3.12 runtime.

#Create threat-intelligence-feed-parser-bucket

1.Create Threat Intelligence S3 Bucket

I created a dedicated S3 bucket to act as the storage interface between the threat-intelligence Lambda functions.

Created the  threat-intelligence-feed-parser  S3 bucket for threat-intelligence data.
Created the threat-intelligence-feed-parser S3 bucket for threat-intelligence data.

2.Configure S3 VPC Endpoint

I configured an S3 Gateway VPC endpoint to provide private S3 connectivity from the ProjectZero VPC.

Configured the S3 Gateway endpoint for the ProjectZero VPC and private route table.
Configured the S3 Gateway endpoint for the ProjectZero VPC and private route table.

#Configure IAM Role for Lambda Functions

I configured a dedicated IAM execution role for the Lambda functions, providing basic execution permissions and scoped read/write access to the threat-intelligence S3 bucket. This allows the functions to interact with the required AWS resources without using hardcoded credentials.

1.Create Lambda Execution Role

I configured AWS Lambda as the trusted service, allowing Lambda functions to assume the execution role.

Configured AWS Lambda as the trusted service for the execution role.
Configured AWS Lambda as the trusted service for the execution role.

2.Attach Basic Lambda Execution Permissions

I created the projectzero-lambda-feed-exec-role and attached the AWSLambdaBasicExecutionRole managed policy to provide basic Lambda execution and CloudWatch logging permissions.

Created the  projectzero-lambda-feed-exec-role  with basic Lambda execution permissions.
Created the projectzero-lambda-feed-exec-role with basic Lambda execution permissions.

3.Configure Scoped S3 Access

I created a custom IAM policy granting the Lambda functions s3:GetObject and s3:PutObject permissions for the threat-intelligence S3 bucket, then attached the policy to the Lambda execution role.

c
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ThreatIntelFeedBucketReadWriteObjects",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::threat-intelligence-feed-log-bucket/*"
    }
  ]
}
Attached the custom S3 read/write policy to the Lambda execution role.
Attached the custom S3 read/write policy to the Lambda execution role.

#Prepare web server PostgreSQL for Lambda Ingest

I prepared the web server and PostgreSQL database to support threat-intelligence ingestion from AWS Lambda. This included restricting database traffic through security groups, enabling PostgreSQL network connectivity, configuring authenticated database access, and preparing the threat-intel-app to securely connect to projectzerodb.

1.Configure Lambda-to-PostgreSQL Network Access

I configured a dedicated security group for the Lambda functions and allowed PostgreSQL traffic from the Lambda security group to the web server on TCP port 5432.

Configured the dedicated Lambda security group within the ProjectZero VPC.
Configured the dedicated Lambda security group within the ProjectZero VPC.
Allowed inbound PostgreSQL traffic on port 5432 from the Lambda security group to the web server.
Allowed inbound PostgreSQL traffic on port 5432 from the Lambda security group to the web server.

2.Configure PostgreSQL Network Listener

I updated PostgreSQL to listen for network connections so the database could accept traffic from Lambda within the VPC.

sudo nano /etc/postgresql/17/main/postgresql.conf

Configured PostgreSQL to listen for connections beyond localhost, enabling database access from the Lambda workload.
Configured PostgreSQL to listen for connections beyond localhost, enabling database access from the Lambda workload.

3.Authorize Lambda Database Access

I configured PostgreSQL host-based authentication to allow the webapp_rw account to connect to projectzerodb from the VPC network using TLS and SCRAM-SHA-256 authentication.

sudo nano /etc/postgresql/17/main/pg_hba.conf

Authorized the  webapp_rw  account to access  projectzerodb  from the VPC network using TLS and SCRAM-SHA-256 authentication.
Authorized the webapp_rw account to access projectzerodb from the VPC network using TLS and SCRAM-SHA-256 authentication.

4.Prepare Threat Intelligence Application

I deployed the updated threat-intel-app to the web server and configured the application with the PostgreSQL connection parameters required to access projectzerodb.

Add .env File to threat-intel-app

I created a .env file to define the application's PostgreSQL connection settings.

plain text
DB_HOST='Private IP Address'
DB_PORT=5432
DB_NAME=projectzerodb
DB_USER='webapp_rw'
DB_PASSWORD='PASSWORD'
DB_SSLMODE=require
Configured the threat-intelligence application to connect to projectzerodb using the restricted webapp_rw account with SSL required.

#Public Threat Intelligence Feed Parser

I deployed a Lambda function to retrieve data from public threat-intelligence sources, process the feeds, and write normalized JSON snapshots to the S3 threat-intelligence bucket. I configured the Lambda runtime, execution role, environment variables, dependencies, and deployment package, then validated the workflow by invoking a test feed and confirming that the resulting JSON objects were successfully generated and stored in S3.

1.Create Public Threat Intelligence Lambda

I created the public-threat-intelligence-feed-parser Lambda function using Python 3.12 and assigned the existing projectzero-lambda-feed-exec-role execution role.

Created the  public-threat-intelligence-feed-parser  Lambda function with Python 3.12 and the dedicated Lambda execution role.
Created the public-threat-intelligence-feed-parser Lambda function with Python 3.12 and the dedicated Lambda execution role.

2.Configure Lambda Runtime

I configured the function with 256 MB of memory and a one minute timeout to support external threat-intelligence retrieval and processing.

Screenshot from project documentation
Configured the Lambda function with 256 MB of memory and a one minute execution timeout.
Configured the Lambda function with 256 MB of memory and a one minute execution timeout.

3.Configure Environment Variables

I configured the environment variables used by the Lambda function to define the S3 output destination and threat-intelligence feed settings.

VariableRequiredDescription
EC2_ENDPOINTYesPostgreSQL host reachable from the private web subnet (typically the private IP of projectzero-prod-websvr-public, e.g. 10.0.x.x).
EC2_DB_NAMENoDatabase name (default in code: projectzerodb).
EC2_USERNAMEYesDatabase user (e.g. webapp_rw or pz_dbadmin per your lab).
EC2_PASSWORDYesUser password (prefer Secrets Manager or Parameter Store in production).
EC2_PORTNoDefault 5432.
EC2_SSLMODENoDefault require; use disable only if your lab explicitly uses non-TLS local access.

Database Ingest Configuration Example

The database-ingest Lambda uses the following environment variable structure to establish an authenticated PostgreSQL connection from the Lambda function.

plain text
EC2_ENDPOINT=10.0.0.240
EC2_DB_NAME=projectzerodb
EC2_USERNAME=webapp_rw
EC2_PASSWORD=your-secret
EC2_PORT=5432
EC2_SSLMODE=require

For the public threat-intelligence parser, I configured FEED_OUTPUT_BUCKET to define the S3 bucket where processed feed data is written.

Configured the S3 output bucket used by the Lambda function to store processed threat-intelligence data.
Configured the S3 output bucket used by the Lambda function to store processed threat-intelligence data.

4.Deploy Lambda Function Code

I uploaded the deployment package containing the Python modules used to retrieve, process, and store public threat-intelligence feeds.

Uploaded the packaged threat-intelligence parser code to the Lambda function.
Uploaded the packaged threat-intelligence parser code to the Lambda function.

5.Verify Lambda Function Code

I verified that the deployment package was successfully extracted and that the Lambda function contained the handler and individual threat-intelligence feed modules required by the parser.

Verified that the Lambda handler and threat-intelligence feed modules were successfully deployed.
Verified that the Lambda handler and threat-intelligence feed modules were successfully deployed.

6.Attach Shared Lambda Layer

I attached the threat-intel-lambda-layer to provide the third party Python dependencies required by the feed processing modules.

Attached the shared Lambda layer containing the external Python dependencies required by the parser.
Attached the shared Lambda layer containing the external Python dependencies required by the parser.

7.Create Test Event

I created a test event using the security_news_rss panel key with a maximum of five results to validate the feed-processing workflow.

Created a test invocation for the  security_news_rss  threat-intelligence feed.
Created a test invocation for the security_news_rss threat-intelligence feed.

8.Validate Lambda Execution

I sccessfully invoked the Lambda function and verified that the processed threat-intelligence data was written to the configured S3 bucket. The function created JSON objects for the requested feeds, confirming that the parser could retrieve the source data, process it, and store the resulting output in S3.

Verified that the Lambda function successfully generated and stored the processed threat-intelligence feeds as JSON objects in the S3 bucket.
Verified that the Lambda function successfully generated and stored the processed threat-intelligence feeds as JSON objects in the S3 bucket.

#Private DB Threat Intelligence Feed Pull

I deployed the private-db-threat-intelligence-feed-pull Lambda function to ingest the JSON threat-intelligence objects generated by the public parser into the PostgreSQL dashboard.panel_feed table. I configured the function with database connection parameters, required dependencies, VPC networking and execution permissions, and an S3 event trigger so new threat-intelligence objects could be automatically retrieved from S3 and written to the database. I validated the complete ingestion workflow with a successful statusCode: 200, confirming S3 object retrieval, private PostgreSQL connectivity, and successful database insertion.

1.Create Private Database Ingest Lambda

I created the private-db-threat-intelligence-feed-pull Lambda function using Python 3.12 and assigned the existing projectzero-lambda-feed-exec-role. This function serves as the private ingestion component of the pipeline, processing JSON objects generated by the public threat-intelligence parser and writing the data to PostgreSQL.

Created the  private-db-threat-intelligence-feed-pull  Lambda function using Python 3.12 and the dedicated Lambda execution role.
Created the private-db-threat-intelligence-feed-pull Lambda function using Python 3.12 and the dedicated Lambda execution role.

2.Configure Lambda Runtime

I configured the function with 256 MB of memory and a one-minute timeout to provide sufficient resources for retrieving JSON objects from S3 and inserting the processed data into PostgreSQL.

Configured the private ingestion Lambda with 256 MB of memory and a one-minute execution timeout.
Configured the private ingestion Lambda with 256 MB of memory and a one-minute execution timeout.

3.Configure Database Environment Variables

I configured environment variables containing the PostgreSQL connection parameters required by the Lambda function. Separating these values from the application code allows the database endpoint, database name, credentials, port, and TLS settings to be managed independently from the deployment package.

VariableRequiredDescription
EC2_ENDPOINTYesPostgreSQL host reachable from the private web subnet (typically the private IP of projectzero-prod-websvr-public, e.g. 10.0.x.x).
EC2_DB_NAMENoDatabase name (default in code: projectzerodb).
EC2_USERNAMEYesDatabase user (e.g. webapp_rw or pz_dbadmin per your lab).
EC2_PASSWORDYesUser password (prefer Secrets Manager or Parameter Store in production).
EC2_PORTNoDefault 5432.
EC2_SSLMODENoDefault require; use disable only if your lab explicitly uses non-TLS local access.

Database Connection Configuration

plain text
EC2_ENDPOINT=10.0.0.240
EC2_DB_NAME=projectzerodb
EC2_USERNAME=webapp_rw
EC2_PASSWORD=your-secret
EC2_PORT=5432
EC2_SSLMODE=require
Configured the PostgreSQL connection parameters used by the private Lambda function to authenticate and connect to  projectzerodb .
Configured the PostgreSQL connection parameters used by the private Lambda function to authenticate and connect to projectzerodb.

4.Deploy Private Lambda Function Code

I uploaded the deployment package containing the Python modules responsible for retrieving threat-intelligence JSON objects from S3 and ingesting the data into PostgreSQL.

Uploaded the packaged  private-db-threat-intelligence-feed-pull  code to the Lambda function.
Uploaded the packaged private-db-threat-intelligence-feed-pull code to the Lambda function.

5.Attach Lambda Dependency Layer

I attached the custom threat-intel-lambda-layer, providing the Python dependencies required by the function, including the PostgreSQL client library used to connect to the database.

Attached the shared Lambda dependency layer required by the private ingestion function.
Attached the shared Lambda dependency layer required by the private ingestion function.

6.Enable Lambda VPC Access

I added AWSLambdaVPCAccessExecutionRole to the Lambda execution role, allowing the function to create and manage the network interfaces required to operate inside the VPC.

Added VPC execution permissions required for private network connectivity.
Added VPC execution permissions required for private network connectivity.

7.Configure S3 Event Trigger

I configured the threat-intelligence S3 bucket as an event source, allowing newly created threat-intelligence objects to automatically invoke the private ingestion Lambda. This creates an event-driven pipeline between S3 and the private Lambda function, automatically processing new threat-intelligence data as it becomes available.

Configured S3 object creation events to trigger the private database ingestion Lambda.
Configured S3 object creation events to trigger the private database ingestion Lambda.

8.Validate S3-to-PostgreSQL Ingestion

I tested the complete ingestion workflow and received a statusCode: 200, confirming the Lambda function executed successfully. The execution logs verified that the function retrieved the threat-intelligence JSON object from S3, connected to PostgreSQL over the private VPC network, and inserted the processed data into dashboard.panel_feed.

Successful Lambda invocation returned  statusCode: 200 , validating S3 object retrieval, private PostgreSQL connectivity, and threat-intelligence database ingestion.
Successful Lambda invocation returned statusCode: 200, validating S3 object retrieval, private PostgreSQL connectivity, and threat-intelligence database ingestion.

#Deploy Threat Intelligence Application

I deployed the Threat Intelligence Dashboard on the EC2 web server and validated the complete data flow from the public threat-intelligence feed to the application. The public Lambda processed the security_news_rss feed and stored the JSON output in S3, which triggered the private Lambda to ingest the data into PostgreSQL. I then accessed the deployed application and confirmed the Security News RSS data was successfully displayed on the dashboard.

1.Build and Launch Threat Intelligence Application

I built the updated threat-intel-app and launched the Astro application on the web server, exposing the application through the server's network interface.

npm run build

Successfully built the threat-intelligence application and generated the production-ready Astro build.
Successfully built the threat-intelligence application and generated the production-ready Astro build.

npm run dev -- --host

Launched the Astro application on the web server and confirmed it was listening on port  4321  through the server’s network interface.
Launched the Astro application on the web server and confirmed it was listening on port 4321 through the server’s network interface.

2.Populate Threeat Intelligence Feeds

I invoked the public threat-intelligence parser using the security_news_rss feed to generate data for the threat-intelligence application. The invocation returned a statusCode: 200, confirming that the RSS feed was successfully retrieved, processed, and written as a JSON object to S3 for downstream ingestion into the application.

Successfully processed the  security_news_rss  feed with a  statusCode: 200 , generating the JSON data used to populate the security news panel.
Successfully processed the security_news_rss feed with a statusCode: 200, generating the JSON data used to populate the security news panel.

3.Trigger Private Database Ingestion

After the public parser wrote the security_news_rss JSON object to S3, the configured S3 event notification automatically triggered the private-db-threat-intelligence-feed-pull Lambda function. The private Lambda then retrieved the JSON object and ingested the threat-intelligence data into PostgreSQL for use by the dashboard.

Screenshot from project documentation

4.Access the Deployed Application

I retrieved the public IPv4 address of the EC2 web server to access the deployed threat-intelligence application from my browser on port 4321.

Retrieved the public IPv4 address of the EC2 instance hosting the threat-intelligence application.
Retrieved the public IPv4 address of the EC2 instance hosting the threat-intelligence application.

5.Verify Threat Intelligence Dashboard

I accessed the deployed Threat Intelligence Dashboard and confirmed that the Security News RSS panel was populated with the feed data processed by the Lambda pipeline. This validated the complete flow from the public feed parser, through S3 and the private ingestion Lambda, into PostgreSQL and finally the web application.

Successfully loaded the Threat Intelligence Dashboard and verified that the Security News RSS data was retrieved from PostgreSQL and displayed by the application.
Successfully loaded the Threat Intelligence Dashboard and verified that the Security News RSS data was retrieved from PostgreSQL and displayed by the application.

6.Remove Temporary Public Application Access

After validating the deployment, I removed the temporary inbound rule for TCP port 4321 from the web server security group to prevent unnecessary public access to the application.

The port was only required to validate the application directly from the browser. Removing the rule after testing reduces the exposed attack surface of the EC2 instance.

Removed the temporary TCP  4321  inbound rule after successfully validating the application.
Removed the temporary TCP 4321 inbound rule after successfully validating the application.

#Why This Matters

This section brought the individual AWS components together into a complete serverless threat-intelligence pipeline. I configured the Lambda environment, IAM permissions, S3 storage, VPC connectivity, and PostgreSQL access needed for the functions to communicate securely. The public Lambda retrieved threat-intelligence data and stored it as structured JSON objects in S3, while S3 events triggered the private Lambda to retrieve those objects and ingest them into PostgreSQL. Finally, I deployed the threat-intelligence application and verified that the stored data was successfully displayed in the dashboard.

This demonstrated how Lambda, IAM, S3, VPC networking, security groups, event-driven triggers, PostgreSQL, and a web application can work together to automate the collection, processing, storage, and presentation of security data.