Today, in a world where almost everybody is going cloud-native, managing infrastructure efficiently as well as securely are essential practices for shaping up reliable systems. Infrastructure as Code (IaC) has revolutionized the way we handle infrastructure configurations and provisioning, however with automation we also need to ensure that security is in place at every stage of the pipeline. This is where DevSecOps & GitOps play a major role.
DevSecOps bakes security into the development and deployment pipeline, helping to ensure that security is “shifted left” and built in from the start. GitOps approach to treating Git as source-of-truth for both application and infrastructure code leading to version-controlled, automatic deployments.
This article will help you to automate security scanning for IaC using tfsec, and add the detection to your GitLab CI/CD pipeline. We will also go through an example of setting up an EC2 instance on AWS, but this can be generalized over any infrastructure managed using IaC.
Prerequisites
Before diving into the implementation, ensure you have the following:
- AWS Account: An AWS account to work with.
- GitLab Account: A GitLab account with a project repository where you can configure GitLab CI/CD and store the code needed for the demo.
- GitLab Runners: Make sure you have GitLab Runners set up to execute the pipeline jobs. The runner should support Docker or GitLab-managed runners.
- OIDC Setup: Set up OpenID Connect (OIDC) between GitLab and AWS to securely authenticate and avoid long-lived credentials. This includes:
- Creating an OIDC provider in AWS for GitLab.
- Creating an IAM role associated with the OIDC provider that has the necessary permissions to run your pipeline tasks.
- Note the Role ARN of the IAM role and add it as a variable named ROLE_ARN in your GitLab CI/CD settings.
- Terraform Installed: While the pipeline will run Terraform, having it installed locally will help troubleshoot any issues.
- Terraform Remote State Setup with S3 Bucket: Ensure you have a remote state setup using an S3 bucket to securely manage your Terraform state files during CI/CD execution on GitLab runners. It is recommended to enable versioning and encryption for the S3 bucket to enhance security. Additionally, configure the necessary IAM permissions to allow Terraform to read and write to the S3 bucket.
Why should you implement IaC Scanning?
Infrastructure as Code (IaC) is all about describing your infrastructure with code to make sure deployments are version-controlled, automated and reproduce-able. Running a scan of your IaC configurations before deploying will help you identify any security vulnerabilities upfront and prevent them from hitting your production.
Key Benefits of IaC
- Version Control – Record all changes to your infrastructure in Git, allowing you to roll back or audit at any time.
- Collaboration — Infrastructure as code enables teams to collaborate on the infrastructure in the same way they work together on application code.
- Rollback: Something wrong went wild, then you can roll back to a safe state of baseline by reverting the code.
- Parallel Development – Multiple team members can work on isolated infrastructure components.
What is DevSecOps and GitOps?
When we refer to DevSecOps, that means security is integrated in the lifecycle stages of development. By incorporating security tools directly into the IaC pipeline, you are able to identify potential vulnerabilities as early in the process as possible and long before they reach deployment.
GitOps, as a way of doing operational work around code makes Git the source of truth for what is currently running and uses CI/CD to drive changes to the desired state. All infrastructure is done in Git, reviewed and applied automatically by the GitLab pipelines on each commit.
Tools for IaC Security:
- tfsec: A static analysis tool that scans Terraform for security misconfigurations. It identifies common mistakes, such as improper IAM role configurations or unprotected resources.
- Checkov: Another IaC security tool that supports Terraform, CloudFormation, and Kubernetes. It provides policy enforcement, ensuring compliance with best practices and industry standards.
Links to resources:
Use Case: EC2 provisioning with Security Scanning
Step 1: Terraform Code for EC2 Instance Provisioning
We’ll start by defining the Terraform code for provisioning an EC2 instance.
Terraform Configuration (main.tf
)
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.3"
}
}
backend "s3" {
bucket = "iac-infra-bucket" # Replace with your S3 bucket name
key = "iac-infra-bucket/state.tfstate" # Replace with your S3 path
region = "us-east-1"
}
}
variable "env_prefix" {
description = "Environment prefix (e.g., dev, prod)"
type = string
default = "dev"
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
environment = var.env_prefix
terraform = "true"
}
}
}
# Security Group
resource "aws_security_group" "ec2_security_group" {
name = "${var.env_prefix}-ec2-sg"
description = "Allow HTTP traffic for EC2"
ingress {
description = "Allow HTTP traffic from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Restrict this if necessary
}
egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1" # Allow all traffic
cidr_blocks = ["0.0.0.0/0"] # Restrict this if necessary
}
}
# EC2 Instance
resource "aws_instance" "ec2_instance" {
ami = "ami-0ebfd941bbafe70c6" # Change as needed
instance_type = "t2.micro"
security_groups = [
aws_security_group.ec2_security_group.name,
]
# Encrypted root block device
root_block_device {
volume_size = 8
volume_type = "gp2"
encrypted = true # Enable encryption
}
metadata_options {
http_tokens = "required" # Enforce the use of IMDSv2
http_endpoint = "enabled"
}
tags = {
Name = "${var.env_prefix}-ec2-instance"
}
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "Welcome to the EC2 instance" > /var/www/html/index.html
EOF
}
# Output the public IP of the EC2 instance
output "ec2_public_ip" {
description = "The public IP of the EC2 instance"
value = aws_instance.ec2_instance.public_ip
}
Step 2: GitLab CI/CD Pipeline Configuration
We will automate the security scanning, and EC2 instance provisioning with GitLab CI/CD.
GitLab CI Configuration (.gitlab-ci.yml
)
variables:
AWSCLI_VERSION: "latest" # Replace with your preferred AWS CLI Image version
TERRAFORM_VERSION: "1.9" # Replace with your preferred Terraform Image version
TFSEC_VERSION: "v1.28.10" # Replace with your preferred TFSec Image version
AWS_DEFAULT_REGION: "us-east-1" # Replace with your preferred AWS region
TF_VAR_env_prefix: "test"
stages:
- authenticate
- iac_security_scan
- infra_provisioning
# Job to authenticate with AWS using OIDC
authenticate:
stage: authenticate
image:
name: amazon/aws-cli:${AWSCLI_VERSION}
entrypoint: [""]
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
before_script:
- >
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
$(aws sts assume-role-with-web-identity
--role-arn ${ROLE_ARN}
--role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
--web-identity-token ${GITLAB_OIDC_TOKEN}
--duration-seconds 3600
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
--output text)) || exit 1
- export AWS_DEFAULT_REGION="us-east-1"
script:
- echo "AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID" > aws_credentials.env
- echo "AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY" >> aws_credentials.env
- echo "AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN" >> aws_credentials.env
- echo "AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION" >> aws_credentials.env
artifacts:
reports:
dotenv: aws_credentials.env
# Job to run TFSec security scan
tfsec:
stage: iac_security_scan
image:
name: aquasec/tfsec:${TFSEC_VERSION}
entrypoint: [""]
script:
- tfsec . --format json --out tfsec.json --soft-fail
allow_failure: true
artifacts:
when: always
paths:
- tfsec.json
# Job to run Terraform commands
terraform:
stage: infra_provisioning
image:
name: hashicorp/terraform:${TERRAFORM_VERSION}
entrypoint: [""]
needs:
- authenticate
- tfsec
script:
- terraform init
- terraform validate
- terraform plan -out=tfplan
- terraform apply -auto-approve tfplan
dependencies:
- authenticate
Step 3: Executing the Flow
In this step, we will walk through the pipeline stages to provision an EC2 instance and ensure security scanning is performed automatically.
- AWS OIDC Authentication
- Execution: GitLab CI authenticates with AWS using OIDC to retrieve temporary credentials.
- Outcome: AWS credentials are securely generated and stored.
- Running Security Scan
- Execution: The tfsec job scans the Terraform configuration for security vulnerabilities.
- Outcome: A report (tfsec.json) is generated, flagging any issues. You can review and address them before deployment.
- Provisioning Infrastructure with Terraform**
- Execution: Terraform initializes, validates the code, generates a plan, and applies it to provision the EC2 instance.
- Outcome: Terraform provisions the EC2 instance and security group, and outputs the instance’s public IP.
By the end of this process, you will have a fully automated pipeline that scans for security issues and provisions infrastructure using Terraform on AWS.
Security Analysis of EC2 Provisioning
By running tfsec on our Terraform code for the EC2 instance provisioning, we found few important discoveries. The key issues highlighted are summarised below;
{
"results": [
{
"rule_id": "AVD-AWS-0104",
"long_id": "aws-ec2-no-public-egress-sgr",
"rule_description": "An egress security group rule allows traffic to /0.",
"rule_provider": "aws",
"rule_service": "ec2",
"impact": "Your port is egressing data to the internet",
"resolution": "Set a more restrictive cidr range",
"links": [
"https://aquasecurity.github.io/tfsec/v1.28.10/checks/aws/ec2/no-public-egress-sgr/",
"https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group"
],
"description": "Security group rule allows egress to multiple public internet addresses.",
"severity": "CRITICAL",
"warning": false,
"status": 0,
"resource": "aws_security_group.ec2_security_group",
"location": {
"filename": "/builds/jvelliah/gitlab-iac-security-scanning/main.tf",
"start_line": 51,
"end_line": 51
}
},
{
"rule_id": "AVD-AWS-0107",
"long_id": "aws-ec2-no-public-ingress-sgr",
"rule_description": "An ingress security group rule allows traffic from /0.",
"rule_provider": "aws",
"rule_service": "ec2",
"impact": "Your port exposed to the internet",
"resolution": "Set a more restrictive cidr range",
"links": [
"https://aquasecurity.github.io/tfsec/v1.28.10/checks/aws/ec2/no-public-ingress-sgr/",
"https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule#cidr_blocks"
],
"description": "Security group rule allows ingress from public internet.",
"severity": "CRITICAL",
"warning": false,
"status": 0,
"resource": "aws_security_group.ec2_security_group",
"location": {
"filename": "/builds/jvelliah/gitlab-iac-security-scanning/main.tf",
"start_line": 43,
"end_line": 43
}
}
]
}
We have a lot to gain by tackling these findings, and that would certainly help make our EC2 provisioning much more secure. Unchecked, these vulnerabilities can provide opportunities for malicious hackers to compromise critical infrastructure. Following the suggested activities will improve the security and reliability of your infrastructure including encrypting, access restricting, and adding descriptions to security group rules.
Note: To avoid incurring unnecessary costs, it is highly recommended to destroy all the resources once they are no longer needed. AWS resources such as EC2 instances, S3 buckets, and others may continue to run and accumulate costs if not properly terminated.
If you want to destroy resources from within a GitLab CI/CD pipeline, you can create a new job in your .gitlab-ci.yml file to run the terraform destroy command.
destroy:
stage: destroy
image:
name: hashicorp/terraform:${TERRAFORM_VERSION}
entrypoint: [""]
needs:
- authenticate
before_script:
- source aws_credentials.env # Load AWS credentials
script:
- terraform init # Reinitialize Terraform if needed
- terraform destroy -auto-approve # Destroy resources without asking for confirmation
dependencies:
- authenticate
Conclusion
This demonstration highlights the power of combining DevSecOps and GitOps principles to automate infrastructure deployment securely. By integrating security scanning tools like tfsec into the pipeline, we ensure that potential security issues are caught before they ever reach production.
Key Takeaways
- Our Security: Left Shifted; by embedding tools such as tfsec into the pipeline, we can detect vulnerabilities sooner allowing for faster remediation well before deployment.
- Doing Infrastructure as Code with GitOps: keep Git as your single source of truth maintaining/ versioning all modifications to infrastructure through CI/CD pipelines in GitLab.
- IaC Workflows: Terraform as a Infrastructure as Code that ensures consistency, collaboration and allows rollback easily with GitLab to automate the deployment and security scanning process.
- Real Time scanning: Tools like tfsec can be integrated to provide real time feedbacks on your IaC misconfigurations hence a secure deployment without any manual intervention.
Combining DevSecOps, GitOps and IaC you can create secure, scalable and automated infrastructure pipelines. From EC2 instances to databases to Kubernetes clusters integrating security from the start makes sure that by the time that infrastructure reaches production, it is rock solid and secure.