October 25, 2026
Deploy IaC securely
One of the amazing technical stack that any software engineer usually knows is Infrastructure as Code “IaC” with this powerful it comes…

By Ala Alhaj
4 min read
One of the amazing technical stack that any software engineer usually knows is Infrastructure as Code "IaC" with this powerful it comes responsibility ! YES, everyone should learn how to be responsible and that is why Security comes into place ! So I myself as senior security engineer comes into the picture and share my humble experience .
Few things I would like to ask as security engineer:
- Do we have different tiers to deploy?
- Where is the state file stored ? Who can read it ?
- Who deploys and maintain deployment ?
- What kind of data we are processing?
- If this deployment goes wrong, what is the worst thing it can touch?
The questions list could going-on for every possible scenario and I only wanted to point the most concerned ones. To demonstrate this , I would like to use an example and then we can answer the questions.
"Everything should be made as simple as possible, but not simpler." — Albert Einstein
Serverless function — Upcoming movies service
We are asked to develop serverless function and doing the following: store data in a storage and send data. I am keeping this very high-level solution to keep things simpler and straightforward. Here the tools that I am going to demonstrate the work: Amazon Web Services (AWS) and Pulumi - Go language SDK, this is preference but for sure you can apply this in other Cloud Service providers.
Pre-requsits:
- Pulumi CLI
- Go language installed in your machine.
- Access to internet
The setup is pretty easy in Pulumi CLI, kick off the creation and then setup the project however you want. After the template was created and assume that service is developed and stored within the same repository , of course we must separate IaC and service source code. Here it looks in my case:
Let us answer the questiones I asked and apply them into this example:
Q: Do we have different tiers to deploy?
A: Yep, it is always recommended to separate your development from production and this includes all the components of the service: Secret Manager, secrets, state files, naming conventions, directory , and permissions. For example, we create separate yaml files that references to each enviorment:
Q: Where is the state file stored ? Who can read it ?
A: The state file should not store locally or in publicly exposed storage. Consult this with your security member to where you can store this state file, best practices is to store the state file in dedicate storage other than the service storage. This can be done in your CI/CD workflow .
Alert: don't store the encryption key in the same YAML FILE ! Use your Secret Manager/KMS to store the key.
Q: Who deploys and maintain deployment ?
A: It is always recommended to have separate account with least privilage and dedicate to deploy your IaC and maintain this.
Q: What kind of data we are processing?
A: Movies ! We are going to fetch data from somewhere else , let us assume trust source and store the data somewhere else, in my case Bucket. We clearly see that we have 2 things that we do : data in-transit and data at-rest. The following as security engineer would suggest to you configure your IaC
- Enable only HTTPs in your serverless function , strongly recommend to set it up TLS 1.2+.
- Enable encryption data at-rest, thankfully CSP provides this option to their users.
- Disable public access to your storage.
Here is best practices documentation that AWS suggests for bucket , here. In our case, we create bucket first and then set it up and for Lambda we must create role and then apply them while provision the Lambda .
package main
import (
"strings"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/lambda"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/secretsmanager"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
cfg := config.New(ctx, "myapp")
bucketName := cfg.Require("bucketName")
functionName := cfg.Require("functionName")
secretName := cfg.Require("secretName")
secret, err := createSecretManager(ctx, "appSecret", secretName)
if err != nil {
return err
}
// Create the S3 Bucket
bucket, err := createSecureBucket(ctx, "secureBucket", bucketName)
if err != nil {
return err
}
// Disable Public Access
err = disablePublicAccess(ctx, bucket.ID())
if err != nil {
return err
}
// Enable Data At-Rest Encryption (AES256)
err = enableEncryption(ctx, bucket.ID(), "AES256")
if err != nil {
return err
}
// Enforce Data In-Transit Encryption (TLS/HTTPS via Policy)
err = enforceTlsPolicy(ctx, bucket.Arn)
if err != nil {
return err
}
lambdaRole, err := createLambdaRole(ctx, "lambdaRole")
if err != nil {
return err
}
fn, err := secureLambdaFunctionUrl(ctx, "secureFunction", functionName, lambdaRole.Arn)
if err != nil {
return err
}
funcUrl, err := createLambdaFunction(ctx, "secureFunctionUrl", functionName, lambdaRole.Arn)
if err != nil {
return err
}
// Export Stack Outputs
ctx.Export("bucketName", bucket.ID())
ctx.Export("bucketArn", bucket.Arn)
ctx.Export("secretArn", secret.Arn)
ctx.Export("functionUrl", funcUrl)
return nil
})
}package main
import (
"strings"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/lambda"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/secretsmanager"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
cfg := config.New(ctx, "myapp")
bucketName := cfg.Require("bucketName")
functionName := cfg.Require("functionName")
secretName := cfg.Require("secretName")
secret, err := createSecretManager(ctx, "appSecret", secretName)
if err != nil {
return err
}
// Create the S3 Bucket
bucket, err := createSecureBucket(ctx, "secureBucket", bucketName)
if err != nil {
return err
}
// Disable Public Access
err = disablePublicAccess(ctx, bucket.ID())
if err != nil {
return err
}
// Enable Data At-Rest Encryption (AES256)
err = enableEncryption(ctx, bucket.ID(), "AES256")
if err != nil {
return err
}
// Enforce Data In-Transit Encryption (TLS/HTTPS via Policy)
err = enforceTlsPolicy(ctx, bucket.Arn)
if err != nil {
return err
}
lambdaRole, err := createLambdaRole(ctx, "lambdaRole")
if err != nil {
return err
}
fn, err := secureLambdaFunctionUrl(ctx, "secureFunction", functionName, lambdaRole.Arn)
if err != nil {
return err
}
funcUrl, err := createLambdaFunction(ctx, "secureFunctionUrl", functionName, lambdaRole.Arn)
if err != nil {
return err
}
// Export Stack Outputs
ctx.Export("bucketName", bucket.ID())
ctx.Export("bucketArn", bucket.Arn)
ctx.Export("secretArn", secret.Arn)
ctx.Export("functionUrl", funcUrl)
return nil
})
}For the sake of completing this work , I did the implementation in this repository, here. However, the implementation is differ from one to another so use your creativity !
Q: If this deployment goes wrong, what is the worst thing it can touch?
A: In our case is accessing the stored data, access to the vendor endpoint using service credential, or even worst than that do privilege escalation ! Ouch !
Hurts right ? Yep , so the best way to get around this is to minimize this access of the deployment , monitoring that you/your organization has in-place to suspicious activity.
In conclusion, this is pretty simple approach this if you individually deploy a IaC, when it comes to deploy within an organization then there must be proper controls , template deployment, and proper-apporval controls. Last but not least reach out to your security team and ask them .
Feel free to reach me out via my LinkedIn: https://www.linkedin.com/in/alahj