Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/agents/terraform-modules.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git
All modules follow the **wrapper module pattern**:

1. Wrap a community module (e.g., `terraform-aws-modules/*`) or native resources.
2. Enforce NHS security baseline (encryption, TLS, no public access, least-privilege iam).
2. Enforce NHS security baseline (encryption, TLS, no public access, least-privilege IAM).
3. Derive naming from `module.this.id` and tagging from `module.this.tags`.
4. Gate creation via `module.this.enabled`.
5. Pin upstream versions explicitly.
Expand Down Expand Up @@ -58,7 +58,7 @@ Every module must enforce:
| Encryption at rest | KMS or service-managed; no unencrypted storage |
| Encryption in transit | TLS required where applicable |
| No public access | Blocked by default at all available toggles |
| iam least-privilege | No `*` actions in policies |
| IAM least-privilege | No `*` actions in policies |
| Logging | Enabled where the service supports it |
| Tagging | All resources via `module.this.tags` |

Expand All @@ -74,7 +74,7 @@ Every module must enforce:
8. Update README when changing module interfaces.
9. Use British English in comments and documentation.
10. Never hard-code secrets, account IDs, or ARNs.
11. Never use `*` in iam policy actions.
11. Never use `*` in IAM policy actions.
12. Never edit `context.tf` directly.

## Exemplar Modules
Expand All @@ -84,3 +84,4 @@ When in doubt, reference:
- `infrastructure/modules/s3-bucket` – full wrapper with security table, locals-based naming
- `infrastructure/modules/iam` – multi-resource wrapper with per-resource iteration and label modules
- `infrastructure/modules/secrets-manager` – simple wrapper with hard-coded security
- `infrastructure/modules/acm` – simple wrapper with opinionated security defaults
3 changes: 3 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ updates:
# BEGIN_AUTOGENERATED_TERRAFORM_UPDATES
- package-ecosystem: "terraform"
directories:
- "infrastructure/modules/acm"
- "infrastructure/modules/api-gateway"
- "infrastructure/modules/aws-backup-destination"
- "infrastructure/modules/aws-backup-source"
Expand All @@ -54,10 +55,12 @@ updates:
- "infrastructure/modules/license-manager"
- "infrastructure/modules/parameter_store"
- "infrastructure/modules/r53-healthcheck"
- "infrastructure/modules/r53"
- "infrastructure/modules/rds-database"
- "infrastructure/modules/rds-gateway-ecs-task"
- "infrastructure/modules/rds-instance"
- "infrastructure/modules/rds-users"
- "infrastructure/modules/rds"
- "infrastructure/modules/s3-bucket"
- "infrastructure/modules/s3"
- "infrastructure/modules/secrets-manager"
Expand Down
3 changes: 2 additions & 1 deletion .github/instructions/pre-commit-hooks.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ git commit -m "type(scope): description"

- `detect-aws-credentials` — detects embedded secrets
- `detect-private-key` — detects leaked private keys
- `scan-secrets` — scans git history for secrets
- `scan-secrets-staged-changes` — scans staged changes for secrets (runs on `git commit`)
- `scan-secrets-whole-history` — scans entire git history for secrets (runs on `pre-commit run --all-files`)
- `terraform_validate` — ensures modules are syntactically valid
- `regenerate-dependabot-config` — ensures Dependabot watches all modules
- `no-commit-to-branch` — enforces PR workflow
Expand Down
27 changes: 17 additions & 10 deletions .github/instructions/terraform-modules.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,21 @@ See `.github/skills/pre-commit-hooks.skill.md` for detailed documentation on all

Every module must contain:

| File | Purpose |
| --- | --- |
| `main.tf` | Primary resource definitions with header comment block |
| `variables.tf` | Input variables with types, descriptions, defaults, validations |
| `outputs.tf` | Output values with descriptions |
| `versions.tf` | `required_version` and `required_providers` |
| `context.tf` | Tags context (copied from `tags/exports/context.tf`) |
| `locals.tf` | Derived/computed values (naming, defaults) |
| `README.md` | Usage documentation with examples |
| File | Purpose | Mandatory |
| --- | --- | --- |
| `main.tf` | Primary resource definitions with header comment block | Yes |
| `variables.tf` | Input variables with types, descriptions, defaults, validations | Yes |
| `outputs.tf` | Output values with descriptions | Yes |
| `versions.tf` | `required_version` and `required_providers` | Yes |
| `context.tf` | Tags context (copied from `tags/exports/context.tf`) | Yes |
| `data.tf` | Data sources (e.g., `data.aws_*`, `data.local_file`) | Only if data sources exist |
| `locals.tf` | Derived/computed values (naming, defaults) | Only if `locals {}` blocks exist |
| `README.md` | Usage documentation with examples | Yes |

**Conditional file guidance:**

- **`data.tf`**: Create this file if the module queries external data (e.g., `data.aws_availability_zones`, `data.aws_ami`). Store all data sources here for clarity.
- **`locals.tf`**: Create this file only if the module defines `locals {}` blocks for computed values, naming logic, or CIDR calculations. If no locals are needed, omit the file entirely.

For any newly created module, `context.tf` must come from `infrastructure/modules/tags/exports/context.tf`, and the copied file must reference `source = "../tags"`.

Expand Down Expand Up @@ -192,7 +198,7 @@ Every module must enforce:
- Encryption at rest (KMS or service-managed) where applicable.
- Encryption in transit (TLS required, deny insecure transport) where applicable.
- No public access by default (block at all available toggles).
- iam least-privilege (no `*` actions in managed policies).
- IAM least-privilege (no `*` actions in managed policies).
- Logging/audit enabled where the service supports it.
- All resources tagged via `module.this.tags`.

Expand Down Expand Up @@ -302,3 +308,4 @@ When in doubt, look at these compliant modules for reference:
- `infrastructure/modules/iam` — Multi-resource wrapper (policies + roles) with per-resource iteration and label modules.
- `infrastructure/modules/secrets-manager` — Simple wrapper with hard-coded security and optional features.
- `infrastructure/modules/kms` — KMS key wrapper with policy enforcement.
- `infrastructure/modules/acm` — Simple wrapper with opinionated security defaults.
9 changes: 7 additions & 2 deletions .github/prompts/new-terraform-module.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,16 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.42"
version = ">= {{ upstream_min_version }}" # Use the upstream module's minimum; >= 6.42 is the platform baseline
}
}
}
```

> **Note:** Use whichever is higher — the upstream community module's stated minimum
> provider version, or the platform baseline of `>= 6.42`. Do not blindly apply the
> platform baseline if the upstream module works correctly at a lower version.

### 5. `context.tf`

Copy from `infrastructure/modules/tags/exports/context.tf`:
Expand Down Expand Up @@ -197,7 +201,7 @@ Before finalising, verify the module enforces:
- [ ] Encryption at rest (KMS or service-managed)
- [ ] Encryption in transit (TLS required) where applicable
- [ ] No public access by default
- [ ] iam least-privilege (no `*` actions)
- [ ] IAM least-privilege (no `*` actions)
- [ ] Logging enabled where the service supports it
- [ ] All resources tagged via `module.this.tags`
- [ ] Creation gated by `module.this.enabled`
Expand All @@ -224,3 +228,4 @@ Reference these for patterns:
- `infrastructure/modules/s3-bucket` — full wrapper with comprehensive security
- `infrastructure/modules/iam` — multi-resource wrapper with per-resource iteration
- `infrastructure/modules/secrets-manager` — simple wrapper with hard-coded security
- `infrastructure/modules/acm` – simple wrapper with opinionated security defaults
78 changes: 61 additions & 17 deletions .github/skills/pre-commit-hooks.skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ AWS credentials detected
**Fix:** Never commit credentials. Use:

- GitHub Secrets for CI/CD
- AWS assume role or iam OIDC federation
- AWS assume role or IAM OIDC federation
- `~/.aws/credentials` for local development

---
Expand Down Expand Up @@ -509,51 +509,94 @@ vale README.md infrastructure/AGENTS.md

### Category 5: Security & Secrets

#### 5.1 `scan-secrets` — Secret Scanning via Gitleaks
#### 5.1 `scan-secrets-staged-changes` — Secret Scanning (Staged Files)

**What it does:** Scans entire git history for embedded secrets (API keys, credentials, etc.) using Gitleaks.
**What it does:** Scans staged changes for embedded secrets (API keys, credentials, etc.) using Gitleaks. Runs automatically on `git commit`.

**When it fails:**

```text
Leaks found: 1
File: .env.example
File: .env
Secret: aws_secret_access_key = "AKIA2EXAMPLE..."
```

**Common false positives:**

- Example/placeholder credentials in `.env.example`
- Test data that looks like credentials

**Fix:**

##### Staged changes: Real secret detected

```bash
# Unstage the file immediately
git reset .env

# Remove or edit to remove the secret
rm .env # or edit to remove secrets

# Stage clean version and commit
git add .env
git commit -m "fix: remove secret from env"
```

##### Staged changes: False positive detected

```bash
# Add to .gitleaksignore
echo "commit-sha:path/to/file:rule-type:line-number" >> .gitleaksignore

# Re-stage and commit
git add .gitleaksignore
git commit -m "chore: ignore false positive"
```

---

#### 5.2 `scan-secrets-whole-history` — Secret Scanning (Complete History)

**What it does:** Scans entire git history for embedded secrets using Gitleaks. Runs on `pre-commit run --all-files` or in CI/CD.

**When it fails:**

```text
Leaks found: 1
File: config/old-backup.tf (in commit abc1234)
Secret: aws_access_key_id = "AKIAIOSFODNN7EXAMPLE"
```

**Common false positives:**

- Example/placeholder credentials in `.env.example`
- Test data that looks like credentials
- Version strings mistaken for IPv4 addresses

**Fix:**

#### Option 1: Real secret (CRITICAL)
##### History scan: Real secret in git history (CRITICAL)

```bash
# Remove the secret immediately
# Remove from history (destructive operation)
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch PATH_TO_FILE' \
'git rm --cached --ignore-unmatch config/old-backup.tf' \
--prune-empty --tag-name-filter cat -- --all

# Force push (warning: destructive)
# Force push to remove from remote
git push origin +main

# IMPORTANT: Regenerate/rotate any exposed credentials
```

#### Option 2: False positive (Add to ignore list)
##### History scan: False positive in git history

```bash
# Get the fingerprint from the error
# Add to .gitleaksignore
echo "commit-sha:path/to/file:rule-type:line-number" >> .gitleaksignore
```

**Manual run:**

```bash
gitleaks detect --verbose
gitleaks detect -i .gitleaksignore # With ignores
# Re-run to verify
pre-commit run scan-secrets-whole-history --all-files
```

---
Expand Down Expand Up @@ -764,7 +807,8 @@ fi
| `check-english-usage` | `.md` | ❌ No | Low | Format |
| `detect-aws-credentials` | All | ❌ No | **CRITICAL** | Security |
| `detect-private-key` | All | ❌ No | **CRITICAL** | Security |
| `scan-secrets` | All | ❌ No | **CRITICAL** | Security |
| `scan-secrets-staged-changes` | All | ❌ No | **CRITICAL** | Security |
| `scan-secrets-whole-history` | All | ❌ No | **CRITICAL** | Security |
| `conventional-commit` | Commit msg | ❌ No | Medium | Commit |
| `no-commit-to-branch` | N/A | ❌ No | High | Git |

Expand Down
9 changes: 8 additions & 1 deletion .github/skills/terraform-module-maintenance.skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,21 @@ terraform-docs markdown . > README.md

Pre-commit hooks will validate that README.md is in sync with the module's variables/outputs on every commit.

## Module File Structure Rules

- `data.tf` is required only if the module uses data sources.
- `locals.tf` is required only if the module defines `locals {}` blocks.
- If these constructs are not used, the corresponding file should be omitted.
- If used, centralise all data sources in `data.tf` and local values in `locals.tf`.

## Compliance & Security Review

After upgrading a module:

1. **Check security baseline**: Verify that enforcement controls haven't been weakened.
2. **Confirm encryption defaults**: Ensure encryption settings still use fixed values.
3. **Confirm public access blocks**: Ensure public access blocks are still enabled.
4. **Confirm iam permissions**: Keep iam policies minimal (no `*` actions).
4. **Confirm IAM permissions**: Keep IAM policies minimal (no `*` actions).
5. **Review upstream breaking changes**: Check community module release notes for incompatible API changes.
6. **Validate outputs**: Ensure stable output names are preserved; consumers depend on them.
7. **Test in context**: If possible, apply the module in a test stack to confirm integration.
Expand Down
10 changes: 8 additions & 2 deletions .github/skills/terraform-module-patterns.skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,13 @@ variable "meaningful_name" {
- Complex types → use `object({})` with `optional()` fields.
- Sensitive values → mark with `sensitive = true`.

## Locals Pattern
## Conditional Module Files

- `data.tf` is mandatory only when data sources exist in the module (for example `data.aws_*`, `data.local_file`, `data.external`).
- `locals.tf` is mandatory only when the module defines one or more `locals {}` blocks.
- When present, keep all data sources in `data.tf` and all local values in `locals.tf`.

## Locals Pattern (When Locals Are Needed)

```hcl
################################################################
Expand Down Expand Up @@ -284,7 +290,7 @@ module that enforces the platform's baseline controls.

\```hcl
module "example" {
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/<name>?ref=main"
source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws.git//infrastructure/modules/<name>?ref=<tag>"

service = "bcss"
environment = "test"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stage-1-pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- name: "shell-and-content"
hooks: >-
shellcheck check-file-format check-markdown-format
check-english-usage scan-secrets
check-english-usage scan-secrets-whole-history
- name: "terraform-format-lint"
hooks: >-
generate-terraform-providers terraform_fmt terraform_tflint
Expand Down
4 changes: 4 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ e876843351a025eb754ec61982c8b7d95deeb709:.pre-commit-config.yaml:ipv4:119
e364bc1869c67729653c7efb4d6169f2294e68de:.pre-commit-config.yaml:ipv4:110
62088509f98ce02ce379adef2168b867eecfb5da:.pre-commit-config.yaml:ipv4:110
a3fa25da4e8f9eaa2e28c29f6196f23bfe87a58d:.pre-commit-config.yaml:ipv4:119
# Historical false positive: example ARN comment in tags/main.tf contained hex-like content
# which triggered the ipv6 rule. Comment updated in later commit; old commits suppressed here.
7b49758d98757e8f404cb2c540c1f146afd6e395:infrastructure/modules/tags/main.tf:ipv6:131
091dcd76884ffd307aee6c6b306b015c065f4896:infrastructure/modules/tags/main.tf:ipv6:131
10 changes: 8 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,14 @@ repos:
files: \.tf(vars)?$
pass_filenames: false

- id: scan-secrets
name: scan-secrets
- id: scan-secrets-staged-changes
name: scan-secrets-staged-changes
entry: bash -c 'check=staged-changes ./scripts/githooks/scan-secrets.sh'
language: system
pass_filenames: false

- id: scan-secrets-whole-history
name: scan-secrets-whole-history
entry: bash -c 'check=whole-history ./scripts/githooks/scan-secrets.sh'
language: system
pass_filenames: false
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Agents **must not**:
When proposing a change, agents should:

- Keep code formatted and idiomatic (Terraform HCL, Bash, YAML).
- Stick to existing patterns — look at compliant modules (`s3-bucket`, `iam`, `secrets-manager`, `kms`) as exemplars.
- Stick to existing patterns — look at compliant modules (`s3-bucket`, `iam`, `secrets-manager`, `kms`, `acm`) as exemplars.
- **Run all pre-commit hooks before committing**: `pre-commit run --all-files` (see `.github/skills/pre-commit-hooks.skill.md` for details on each hook).
- Run `terraform fmt -recursive` before committing.
- Run `terraform validate` in affected module directories.
Expand All @@ -94,7 +94,7 @@ Not all modules in this repository are currently compliant. The following tiers

| Tier | Description | Examples |
| --- | --- | --- |
| **Compliant** | Full wrapper pattern, `context.tf`, security baseline, proper variables/outputs/README | `s3-bucket`, `iam`, `secrets-manager`, `kms`, `sns` |
| **Compliant** | Full wrapper pattern, `context.tf`, security baseline, proper variables/outputs/README | `s3-bucket`, `iam`, `secrets-manager`, `kms`, `acm` |
| **Partially compliant** | Has `context.tf` but may be missing validation, README, or security hardening | `lambda`, `ecr`, `vpc` |
| **Legacy** | Older modules that predate the current conventions; may lack `context.tf` entirely | Various older modules |

Expand Down
Loading
Loading