Secrets Management
This guide explains how to securely handle sensitive credentials when deploying the Internal Scanner via the AWS Terraform module. For chart-level secret handling that applies to any cluster (kubectl, external-secrets-operator, sealed-secrets, etc.), see the Helm Secrets Management guide; this page focuses on how those mechanisms compose with the Terraform module on AWS.
What Are Secrets?
The Internal Scanner requires credentials provided by Detectify:
| Secret | Description | Where to Find |
|---|---|---|
| License Key | Activates your scanner instance | Detectify UI → Internal Scanning Agents |
| Connector API Key | Authenticates with Detectify platform | Detectify UI → Internal Scanning Agents |
| Registry Credentials | Pulls scanner container images | Detectify UI → Internal Scanning Agents |
These credentials are generated by Detectify and have a validity period managed on Detectify’s side. They are static and do not rotate automatically.
Security Principle: Secrets should never exist in plaintext in version control, logs, or anywhere they could be accidentally exposed.
Recommended Approaches
Choose the approach that fits your infrastructure:
| Approach | Best For |
|---|---|
| KMS Encryption | Most deployments — encrypted secrets stored in version control, decrypted at apply time |
| AWS Secrets Manager | Organizations with centralized secrets management policies |
| Bring-your-own Secret | Credentials managed entirely outside Terraform (Vault, external-secrets-operator, sealed-secrets, AWS Secrets Manager + ESO, hand-managed kubernetes_secret, …) — keeps credentials out of Terraform state |
Note: For initial local testing only, you can use a gitignored
terraform.tfvarsfile as shown in the Terraform guide. However, you should set up proper secrets management before deploying to any shared environment.
KMS Encryption (Recommended)
Encrypt secrets with AWS KMS and store the encrypted file in version control. Terraform decrypts them automatically during deployment.
How It Works
Secrets are encrypted locally using AWS KMS and stored as a binary blob in version control. During deployment, Terraform decrypts them automatically using the aws_kms_secrets data source — credentials exist only in memory and are never written to disk.
Step 1: Create a KMS Key
Create a KMS key using the AWS CLI:
aws kms create-key --description "Internal Scanner secrets encryption" --region eu-west-1
aws kms create-alias --alias-name alias/internal-scanner-secrets --target-key-id YOUR_KEY_ID --region eu-west-1Alternatively, use an existing KMS key — just note its alias for the next steps.
Step 2: Create Secrets Directory
mkdir -p secrets/Step 3: Create Plaintext Secrets File
Create secrets/secrets.json:
{
"registry_username": "your-registry-username",
"registry_password": "your-registry-password",
"license_key": "your-license-key",
"connector_api_key": "your-connector-api-key"
}Security: This file will be deleted after encryption. Never commit it.
Step 4: Create Encryption Script
Create secrets/encrypt.sh:
#!/bin/bash
set -e
# ============================================================
# CONFIGURATION - Update these values for your environment
# ============================================================
KEY_ALIAS="alias/internal-scanner-secrets"
REGION="eu-west-1"
# AWS_PROFILE="your-profile" # Uncomment if using named profile
# ============================================================
# File paths (no changes needed)
# ============================================================
SECRETS_FILE="secrets.json"
ENCRYPTED_FILE="secrets.encrypted"
# Validate secrets file exists
if [ ! -f "$SECRETS_FILE" ]; then
echo "Error: $SECRETS_FILE not found"
echo ""
echo "Create it with this structure:"
echo '{'
echo ' "registry_username": "...", '
echo ' "registry_password": "...",'
echo ' "license_key": "...",'
echo ' "connector_api_key": "..."'
echo '}'
exit 1
fi
# Validate JSON syntax
if ! jq empty "$SECRETS_FILE" 2>/dev/null; then
echo "Error: $SECRETS_FILE contains invalid JSON"
exit 1
fi
echo "Encrypting secrets with KMS..."
echo " Key: $KEY_ALIAS"
echo " Region: $REGION"
# Encrypt
aws kms encrypt \
--region "$REGION" \
${AWS_PROFILE:+--profile "$AWS_PROFILE"} \
--key-id "$KEY_ALIAS" \
--plaintext "fileb://$SECRETS_FILE" \
--output text \
--query CiphertextBlob | base64 --decode > "$ENCRYPTED_FILE"
echo ""
echo "Encryption successful!"
echo ""
echo "Next steps:"
echo " 1. Delete plaintext: rm $SECRETS_FILE"
echo " 2. Commit encrypted: git add $ENCRYPTED_FILE"Make executable and run:
chmod +x secrets/encrypt.sh
cd secrets/
./encrypt.shStep 5: Delete Plaintext and Commit
# Delete plaintext (required!)
rm secrets.json
# Commit encrypted file
git add secrets.encrypted encrypt.sh
git commit -m "Add encrypted scanner secrets"Step 6: Configure Terraform Decryption
Create secrets.tf in your main Terraform directory:
#---------------------------------------------------------------
# Secrets Decryption
#---------------------------------------------------------------
# Decrypts KMS-encrypted secrets at runtime.
# The encrypted file is safe to store in version control.
data "aws_kms_secrets" "scanner" {
secret {
name = "secrets_json"
payload = filebase64("${path.module}/secrets/secrets.encrypted")
}
}
locals {
# Parse decrypted JSON
secrets = jsondecode(data.aws_kms_secrets.scanner.plaintext["secrets_json"])
# Extract individual values for use in module
registry_username = local.secrets["registry_username"]
registry_password = local.secrets["registry_password"]
license_key = local.secrets["license_key"]
connector_api_key = local.secrets["connector_api_key"]
}Step 7: Use Decrypted Secrets in Module
Update main.tf:
module "internal_scanner" {
source = "detectify/internal-scanning/aws"
version = "~> 3.0"
# ... network configuration ...
# Secrets (decrypted at runtime)
license_key = local.license_key
connector_api_key = local.connector_api_key
registry_username = local.registry_username
registry_password = local.registry_password
}Step 8: Configure .gitignore
# Terraform state
.terraform/
*.tfstate
*.tfstate.*
# Plaintext secrets - NEVER commit
secrets/secrets.json
*.auto.tfvars
secrets.tfvars
# Encrypted secrets - safe to commit
!secrets/*/secrets.encryptedUpdating Secrets
To update a secret, decrypt the file, edit it, re-encrypt, and commit.
Create secrets/decrypt.sh for this workflow:
#!/bin/bash
set -e
REGION="eu-west-1"
# AWS_PROFILE="your-profile" # Uncomment if using named profile
ENCRYPTED_FILE="secrets.encrypted"
DECRYPTED_FILE="secrets.json"
if [ ! -f "$ENCRYPTED_FILE" ]; then
echo "Error: $ENCRYPTED_FILE not found"
exit 1
fi
echo "Decrypting secrets..."
aws kms decrypt \
--region "$REGION" \
${AWS_PROFILE:+--profile "$AWS_PROFILE"} \
--ciphertext-blob "fileb://$ENCRYPTED_FILE" \
--output text \
--query Plaintext | base64 --decode > "$DECRYPTED_FILE"
echo "Decrypted to: $DECRYPTED_FILE"
echo ""
echo "WARNING: Delete this file after editing!"
echo " Run: rm $DECRYPTED_FILE"AWS Secrets Manager
For organizations with centralized secrets management policies or teams that prefer not to store encrypted files in repositories.
Store Secrets
aws secretsmanager create-secret \
--name "internal-scanner" \
--description "Internal Scanner credentials" \
--secret-string '{
"registry_username": "your-registry-username",
"registry_password": "your-registry-password",
"license_key": "your-license-key",
"connector_api_key": "your-connector-api-key"
}'Terraform Configuration
# secrets.tf
data "aws_secretsmanager_secret_version" "scanner" {
secret_id = "internal-scanner"
}
locals {
secrets = jsondecode(data.aws_secretsmanager_secret_version.scanner.secret_string)
registry_username = local.secrets["registry_username"]
registry_password = local.secrets["registry_password"]
license_key = local.secrets["license_key"]
connector_api_key = local.secrets["connector_api_key"]
}IAM Permissions
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:*:*:secret:internal-scanner/*"
}
]
}For security best practices, see Security & Privacy.
Bring-your-own Secret
Module 3.0 exposes two variables that let the chart skip creating its own Secrets and mount Secrets you manage elsewhere:
var.existing_config_secret— name of anOpaqueSecret with keyslicense-key,connector-api-key,connector-url.var.existing_registry_secret— name of akubernetes.io/dockerconfigjsonSecret.
Both must exist in var.namespace before terraform apply. When either is set, the corresponding inline credentials (license_key, connector_api_key, registry_username, registry_password) become optional.
module "internal_scanner" {
source = "detectify/internal-scanning/aws"
version = "~> 3.0"
name = "detectify-scanner"
vpc_id = "vpc-xxxxx"
private_subnet_ids = ["subnet-xxxxx", "subnet-yyyyy"]
namespace = "scanner"
# Chart reads credentials from these Secrets instead of creating its own.
existing_config_secret = "scanner-config"
existing_registry_secret = "detectify-registry"
}Typical sources for those Secrets:
- external-secrets-operator — recommended for EKS. Store the credentials in AWS Secrets Manager, use an IRSA-backed
ClusterSecretStoreto read them, and letExternalSecretCRs render the two native Kubernetes Secrets. ESO handles rotation. - sealed-secrets — encrypt once, commit to Git, cluster-resident controller decrypts.
- Hand-managed
kubernetes_secret— if you already have Terraform that writes the Secret from some other source of truth, the module just points the chart at it.
The two are independent. A common hybrid is existing_registry_secret (registry creds in Vault / Secrets Manager) alongside inline license_key / connector_api_key decrypted from KMS.
Ordering when writing Secrets from the same Terraform root
The Secrets must exist in var.namespace before the Helm release installs; otherwise the scanner pods crashloop with secret "scanner-config" not found. The module doesn’t know about Secret resources you create elsewhere, so add an explicit depends_on if you write them from the same Terraform configuration:
resource "kubernetes_secret" "scanner_config" {
metadata {
name = "scanner-config"
namespace = "scanner"
}
type = "Opaque"
data = {
"license-key" = local.license_key
"connector-api-key" = local.connector_api_key
"connector-url" = "https://connector.detectify.com"
}
}
module "internal_scanner" {
source = "detectify/internal-scanning/aws"
version = "~> 3.0"
name = "detectify-scanner"
vpc_id = "vpc-xxxxx"
private_subnet_ids = ["subnet-xxxxx", "subnet-yyyyy"]
namespace = "scanner"
existing_config_secret = kubernetes_secret.scanner_config.metadata[0].name
# license_key / connector_api_key not needed — the Secret provides them.
# registry_username / registry_password still required unless
# existing_registry_secret is also set.
registry_username = var.registry_username
registry_password = var.registry_password
depends_on = [kubernetes_secret.scanner_config]
}When the Secret comes from ESO / sealed-secrets / CSI Driver, the operator reconciles asynchronously — you don’t control the order from Terraform. Run terraform apply once to install the operator and the chart together; the chart will initially crashloop until the Secret appears, then converge.
Comparison at a glance
The table below compares how each approach composes with the Terraform module on AWS — i.e. where credentials live, how rotation happens, and what ends up in Terraform state. For the chart-level comparison (any platform, any secrets tool), see Helm Secrets Management → Approach comparison.
| Approach | Secrets live in | Where rotation happens | When to choose |
|---|---|---|---|
| Inline + KMS (above) | Git (encrypted) + Terraform state | Terraform apply | Default AWS path; simple, auditable, but requires re-apply to rotate. |
| Inline + AWS Secrets Manager data source | AWS Secrets Manager; passed through Terraform state | AWS Secrets Manager + next Terraform apply | Centralised secrets store, but credentials still land in state. |
| BYO Secret + ESO → AWS Secrets Manager | AWS Secrets Manager; rendered directly into the cluster | AWS Secrets Manager, ESO refreshInterval | No credentials in Terraform state; continuous rotation. |
| BYO Secret + sealed-secrets | Git (encrypted) + cluster | kubeseal re-seal + GitOps apply | GitOps teams with no external secrets store. |
| BYO Secret + Vault | Vault; rendered by ESO / Vault Secrets Operator | Vault, ESO refreshInterval | Shops standardised on Vault. |
For the worked AWS Secrets Manager + ESO recipe (storing the JSON blobs, IRSA, ClusterSecretStore, the two ExternalSecret CRs, rotation via Reloader), see the Helm Secrets Management guide — the recipe is chart-level and applies identically whether the cluster was created by this module or provisioned separately.
Troubleshooting
”AccessDeniedException” during encryption
Cause: Your IAM user/role lacks kms:Encrypt permission.
Solution: Add an IAM policy granting kms:Encrypt and kms:DescribeKey on your KMS key to your IAM identity, or ensure you’re listed in the KMS key policy.
”AccessDeniedException” during terraform apply
Cause: Pipeline role lacks kms:Decrypt permission.
Solution:
- Verify the role ARN in your KMS key policy
- Check the IAM role has the decrypt policy attached
- Confirm you’re using the correct AWS region
”InvalidCiphertextException”
Cause: Encrypted file corrupted or encrypted with a different key.
Solution:
- Re-encrypt from the original plaintext
- Verify you’re using the same KMS key alias
- Check the file wasn’t modified after encryption
Terraform shows secrets in plan output
Cause: Variables not marked as sensitive = true.
Solution: Ensure all secret variables have sensitive = true in their definition:
variable "license_key" {
type = string
sensitive = true # This prevents display in logs
}Next Steps
- Getting Started - Complete deployment guide
- Configuration - Network, API, and Helm values passthrough
- Scaling - Configure for your workload
- Troubleshooting - Common issues