Terraform infrastructure for deploying a self-hosted GitLab instance on AWS with a security-hardened network design aligned with DoD IL2 requirements. Developers access GitLab directly via HTTPS with native GitLab authentication and mandatory 2FA (TOTP). The public ALB is protected by AWS WAF. All encryption at rest uses Customer Managed KMS Keys with automatic annual rotation.
GitLab runs on a single EC2 instance inside private subnets. Developers connect over HTTPS to an internet-facing Application Load Balancer, which is protected by AWS WAF (6 rule groups: OWASP common rules, known bad inputs, IP reputation, rate limiting, SQLi, and Linux rules). The ALB terminates TLS 1.3 and re-encrypts traffic to the GitLab instance over HTTPS (port 443) using a self-signed certificate. Outbound internet access (for package updates) is routed through a NAT Gateway in the public subnets. Admin access to the instance is via SSM Session Manager only.
AWS service access from the private subnets is handled via 7 VPC endpoints (S3, SSM, SSM Messages, EC2 Messages, Secrets Manager, CloudWatch Logs, KMS), minimizing traffic that traverses the NAT Gateway. All data is encrypted at rest using Customer Managed KMS Keys, and all S3 buckets have public access blocked with lifecycle policies that transition objects to Glacier. All resources are tagged with the DoD IL2 DataClassification tag.
Continuous monitoring is provided by multi-region CloudTrail (CMK-encrypted with CloudWatch Logs integration), GuardDuty (with EBS malware protection), Security Hub (NIST 800-53 v5 and AWS best practices), AWS Config, Amazon Inspector (EC2 CVE scanning), and CloudWatch alarms with SNS alerting. ClamAV provides on-host antimalware scanning with daily signature updates. Automated Lambda functions handle inactive account deactivation (90-day threshold), Secrets Manager root password rotation (90-day cycle), and CISA KEV advisory monitoring.
Customer Managed KMS Keys with automatic annual rotation.
- General key -- S3 buckets, Secrets Manager, CloudWatch Logs, SNS, VPC flow logs, WAF logs, AWS Config, Security Hub
- CloudTrail key -- CloudTrail log encryption (dedicated key policy for
cloudtrail.amazonaws.com) - EBS key -- EC2 root and data volume encryption
VPC, subnets, route tables, NAT Gateway, security groups, VPC endpoints, flow logs, and S3 access logs bucket.
- VPC (
10.0.0.0/16) with DNS hostnames enabled - 2 public subnets and 2 private subnets across 2 AZs
- NAT Gateway (single AZ) with Elastic IP for outbound from private subnets
- Internet Gateway for the public subnets
- Security groups for the ALB, GitLab EC2, and VPC endpoints
- VPC interface endpoints: SSM, SSM Messages, EC2 Messages, Secrets Manager, CloudWatch Logs, KMS
- S3 gateway endpoint via route table
- Total: 7 VPC endpoints (6 interface + 1 gateway)
- VPC flow logs to S3 (CMK-encrypted, 60-second aggregation)
- S3 access logs target bucket for ALB access logs
Multi-region CloudTrail, CloudWatch alarms, and SNS alerting.
- Multi-region CloudTrail logging to S3 with CMK encryption, log file validation, and Glacier lifecycle
- CloudTrail integration with CloudWatch Logs for real-time log analysis
- Unauthorized API call detection via CloudWatch metric filter
- CloudWatch alarms for CPU utilization (>90% for 15 min) and EC2 status check failures
- SNS topic for alarm notifications
EC2 instance, EBS volumes, IAM role, S3 backup bucket, and Secrets Manager secrets.
t3.xlargeEC2 instance running Amazon Linux 2023- 50 GB CMK-encrypted root volume + 100 GB CMK-encrypted gp3 data volume (
/var/opt/gitlab) - IMDSv2 enforced, detailed monitoring enabled
- IAM instance profile with policies for SSM, S3 backups, Secrets Manager, and CloudWatch Logs
- S3 backup bucket with versioning, CMK encryption, and Glacier lifecycle
- Secrets Manager entries for root password and
gitlab-secrets.json(CMK-encrypted)
Internet-facing Application Load Balancer, target group, HTTPS listener, ACM certificate, and access logging.
- Internet-facing ALB spanning 2 public subnets
- HTTPS listener on port 443 with TLS 1.3 policy (
ELBSecurityPolicy-TLS13-1-2-2021-06) - ACM certificate with DNS validation
- Target group forwarding HTTPS port 443 with health checks on
/-/health - Access logs to the S3 access logs bucket (managed by the
networkingmodule) with Glacier lifecycle - Deletion protection enabled
AWS WAF WebACL with managed rules, rate limiting, and logging.
- OWASP common rules (AWS Managed Rules Common Rule Set)
- Known bad inputs rule group
- IP reputation list rule group
- Rate limiting to protect against abuse
- SQL injection rule group
- Linux-specific rule group
- Associated with the internet-facing ALB
- WAF logging to CloudWatch Logs (CMK-encrypted)
GuardDuty, Security Hub, AWS Config, and Amazon Inspector for IL2 continuous monitoring.
- Amazon GuardDuty threat detection with EBS malware protection
- AWS Security Hub with NIST 800-53 v5 and AWS Foundational Security Best Practices standards
- AWS Config recorder and delivery channel (CMK-encrypted S3 bucket)
- Amazon Inspector v2 for EC2 vulnerability scanning (CVEs, CISA KEV)
Automated inactive account deactivation (AC-2(3)).
- Weekly Lambda (EventBridge schedule) deactivates GitLab users inactive >90 days
- Uses GitLab Admin API with PAT stored in Secrets Manager (CMK-encrypted)
- Skips admin and bot accounts; supports
DRY_RUNmode (default: enabled) - SNS notifications on deactivations; CloudWatch error alarm
- Runs in VPC private subnets, reaches GitLab via NAT Gateway
Automated secrets rotation for GitLab root password (IA-5(1)).
- Standard 4-step Secrets Manager rotation Lambda (createSecret, setSecret, testSecret, finishSecret)
- Applies password on EC2 via SSM Run Command with
gitlab-rails runner - Password handoff via temporary SSM SecureString parameters (never in shell args)
- 90-day automatic rotation schedule
- AWSPREVIOUS label preserves rollback capability
CISA Known Exploited Vulnerabilities (KEV) advisory monitoring (SI-5).
- Daily Lambda polls CISA KEV JSON catalog
- Tracks state in DynamoDB; sends SNS notifications for new entries
- Complements Inspector's CISA KEV integration
One-time S3 state backend setup (run independently before the main module).
- S3 bucket for Terraform state with versioning, KMS encryption, and public access block
- DynamoDB table for state locking (
PAY_PER_REQUEST) - Non-current version expiration at 90 days
- Prevent-destroy lifecycle on state bucket
- AWS account with appropriate permissions
- Terraform >= 1.5
- A domain name with a Route53 hosted zone (created by Terraform)
- Access to the parent DNS zone for subdomain NS delegation
cd terraform
cp terraform.tfvars.example terraform.tfvars # edit with your values
terraform init
terraform plan
terraform applyKey variables in terraform.tfvars:
| Variable | Description | Default |
|---|---|---|
aws_region |
AWS region for deployment | us-east-1 |
domain_name |
Domain name for GitLab (e.g., gitlab.example.com) |
-- |
instance_type |
EC2 instance type | t3.xlarge |
data_volume_size |
GitLab data EBS volume size (GB) | 100 |
backup_replication_region |
Region for backup cross-region replication | us-west-2 |
data_classification |
DoD data classification level | IL2 |
use_fips_ami |
Use FIPS-validated AMI (SC-13) | false |
enable_backup_replication |
Cross-region S3 backup replication (CP-6) | false |
alert_email |
Email for CloudWatch alarm notifications | -- |
ACM certificate DNS validation is automated via Route53 -- no manual step required.
Connect to the instance via SSM:
aws ssm start-session --target <instance-id>-
Populate GitLab Admin PAT: Create a GitLab admin Personal Access Token with
apiscope, then store it in Secrets Manager:aws secretsmanager put-secret-value \ --secret-id gitlab/gitlab-admin-pat \ --secret-string "glpat-xxxxxxxxxxxxxxxxxxxx" -
Validate user deactivation Lambda: The Lambda runs in
DRY_RUNmode by default. Test invoke to verify:aws lambda invoke --function-name gitlab-user-deactivation /dev/stdout
Once validated, set
dry_run = falseinterraform.tfvarsand re-apply. -
Verify Inspector scanning: Check AWS Console > Inspector > Settings to confirm EC2 scanning is active.
-
Verify ClamAV: Connect via SSM and verify:
systemctl status clamd@scan freshclam --version
-
Populate root password secret: Store the initial GitLab root password for rotation:
aws secretsmanager put-secret-value \ --secret-id gitlab/root-password \ --secret-string "your-initial-root-password"
DNS is managed via a Route53 hosted zone created by the dns module. The parent zone is in a separate AWS account. After deployment:
- Get the nameservers:
terraform output route53_nameservers - In the parent zone, create NS records:
- Name:
code(or whatever subdomain matches yourdomain_name) - Type: NS
- Values: The 4 nameservers from step 1
- Name:
- Wait for DNS propagation (typically 5-60 minutes)
- ACM certificate validation happens automatically via Route53
| Output | Description |
|---|---|
gitlab_instance_id |
EC2 instance ID |
gitlab_private_ip |
Private IP address |
alb_dns_name |
ALB DNS name |
gitlab_url |
GitLab URL (https://<domain>) |
backup_bucket |
S3 backup bucket name |
ssm_connect_command |
SSM session command |
route53_nameservers |
Route53 NS records for parent zone delegation |
| Document | Description |
|---|---|
| IL2 Compliance Mapping | NIST 800-53 control mapping with implementation details |
| Incident Response Plan | IRP per NIST 800-61r2 (IR-4, IR-6) |
| System Security Plan | SSP per NIST 800-18 (PL-2) |
| Quick Start Guide | Developer onboarding guide |
