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
34 changes: 19 additions & 15 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# AWS Encryption Configuration
# Copy this file to .env and fill in your values
# envault CLI configuration
# Copy this file to .env and fill in your values.
# The Python CLI reads ENVAULT_* variables only.

# AWS Configuration
AWS_PROFILE=default
AWS_REGION=us-east-1
# Required
ENVAULT_KEY_ID=alias/envault
ENVAULT_BUCKET=my-encrypted-files-bucket
ENVAULT_TABLE=envault-state

# S3 Bucket for encrypted file storage
S3_BUCKET=sensitive-docs-XXXXXXXXXXXX
# Required for decrypt, exec, and rotate-key
ENVAULT_ALLOWED_ACCOUNT_IDS=123456789012

# KMS Key Configuration
# For encryption, use the alias
KMS_KEY_ALIAS=alias/s3_key
# Optional
ENVAULT_REGION=us-east-1
ENVAULT_AUDIT_TTL_DAYS=365

# For decryption, use the full ARN
KMS_KEY_ARN=arn:aws:kms:us-east-1:XXXXXXXXXXXX:key/your-key-id-here

# Encryption context (must match on decrypt)
ENCRYPTION_CONTEXT=purpose=backup
# ---------------------------------------------------------------------------
# Legacy shell scripts (code/encrypt.sh, code/decrypt.sh) — deprecated.
# These are NOT read by the envault CLI.
# ---------------------------------------------------------------------------
# S3_BUCKET=sensitive-docs-XXXXXXXXXXXX
# KMS_ACCOUNT_ID=123456789012
# KMS_KEY_ALIAS=alias/s3_key
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ jobs:
run: |
pip install --upgrade pip
pip install pip-audit
# CVE-2026-4539: pygments (dev/docs highlighter, not a runtime dep)
# CVE-2026-3219: pip itself (CI installer), not shipped in the wheel
pip-audit --ignore-vuln CVE-2026-4539 --ignore-vuln CVE-2026-3219

test:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ jobs:
run: |
pip install --upgrade pip
pip install pip-audit
# CVE-2026-4539: pygments (dev/docs highlighter, not a runtime dep)
# CVE-2026-3219: pip itself (CI installer), not shipped in the wheel
pip-audit --ignore-vuln CVE-2026-4539 --ignore-vuln CVE-2026-3219

build:
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- KMS key policy no longer denies `DisableKey`, so a compromised CMK can be frozen during incident response without a CloudFormation change.
- IAM policy no longer grants unused `dynamodb:UpdateItem` / `s3:ListBucket`; S3 object actions are scoped to `encrypted/*`; `sts:GetCallerIdentity` is granted for audit attribution.
- Ops SNS topic is encrypted with the envault CMK.
- `decrypt` refuses to overwrite an existing destination unless `--force` is passed, and checks before creating temp files.
- S3 downloads require a content-addressed key (`encrypted/{aa}/{sha256}/{name}.encrypted`) so a poisoned DynamoDB `s3_key` cannot fetch an arbitrary object.
- Directory-symlink trees are skipped by `os.walk(followlinks=False)` during encrypt.
- `migrate` confines input paths to the import directory and rejects per-component symlinks.
- `ENVAULT_AUDIT_TTL_DAYS` is applied on encrypt, decrypt, exec, rotate-key, and migrate event writes.
- `rotate-key` calls `DescribeKey` on the target CMK before downloading or decrypting anything.
- `last_updated` CAS tokens use microsecond timestamps.
- Dashboard `last_activity` pages the state-index until a CURRENT item survives the filter.
- `exec` warns when the child will inherit `AWS_*` credentials; `--clean-env` remains opt-in.
- Encrypt closes the output fd if the SDK stream fails before `fdopen`.

## [0.2.0] - 2026-07-26

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ envault encrypt INPUT_PATH [--tag KEY=VALUE]... [--force]
envault exec -s IDENTIFIER=VAR [-f IDENTIFIER=VAR]... [--clean-env] -- COMMAND [ARGS]...

# Decrypt by filename or SHA256 hash
envault decrypt IDENTIFIER [-o OUTPUT_DIR] [--version N]
envault decrypt IDENTIFIER [-o OUTPUT_DIR] [--version N] [--force]

# List all encrypted/decrypted files
envault status [--state encrypted|decrypted|all]
Expand Down
790 changes: 97 additions & 693 deletions SECURITY_AUDIT.md

Large diffs are not rendered by default.

37 changes: 26 additions & 11 deletions infra/cdk/stacks/envault_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,15 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs: object) -> Non
)

# Deny key deletion for all principals — requires removing this
# policy statement first (break-glass procedure).
# policy statement first (break-glass procedure). DisableKey is
# intentionally allowed so operators can freeze a compromised CMK
# during incident response without a CloudFormation change.
encryption_key.add_to_resource_policy(
iam.PolicyStatement(
sid="DenyScheduleKeyDeletion",
effect=iam.Effect.DENY,
principals=[iam.AnyPrincipal()],
actions=["kms:ScheduleKeyDeletion", "kms:DisableKey"],
actions=["kms:ScheduleKeyDeletion"],
resources=["*"],
)
)
Expand Down Expand Up @@ -196,20 +198,23 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs: object) -> Non
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:ListBucket",
],
resources=[bucket.bucket_arn, f"{bucket.bucket_arn}/*"],
resources=[f"{bucket.bucket_arn}/encrypted/*"],
),
iam.PolicyStatement(
sid="DynamoDBStateAccess",
actions=[
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:UpdateItem",
],
resources=[table.table_arn, f"{table.table_arn}/index/*"],
),
iam.PolicyStatement(
sid="StsCallerIdentity",
actions=["sts:GetCallerIdentity"],
resources=["*"],
),
],
)

Expand All @@ -222,25 +227,34 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs: object) -> Non
{
"id": "AwsSolutions-IAM5",
"reason": (
"S3 object-level actions (PutObject, GetObject) require"
" bucket/* wildcard. Access is scoped to the single"
" envault bucket."
"S3 object-level actions require a key prefix wildcard."
" Access is scoped to encrypted/* on the single envault"
" bucket — the CLI never lists or reads other prefixes."
),
"applies_to": [
f"Resource::<{bucket.node.id}.Arn>/*",
f"Resource::<{bucket.node.id}.Arn>/encrypted/*",
],
},
{
"id": "AwsSolutions-IAM5",
"reason": (
"DynamoDB GSI queries require table/index/* wildcard."
" Access is scoped to the single envault table and"
" only allows read/write operations."
" only allows PutItem/GetItem/Query."
),
"applies_to": [
f"Resource::<{table.node.id}.Arn>/index/*",
],
},
{
"id": "AwsSolutions-IAM5",
"reason": (
"sts:GetCallerIdentity does not support resource-level"
" authorization; Resource * is required by the API."
" Used only to attribute audit events to the caller."
),
"applies_to": ["Resource::*"],
},
],
)

Expand All @@ -252,6 +266,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs: object) -> Non
"EnvaultOpsTopic",
display_name="envault operational alerts",
enforce_ssl=True,
master_key=encryption_key,
)

# DynamoDB throttle alarm
Expand All @@ -269,7 +284,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs: object) -> Non
# operations envault actually calls to stay within the 10-metric
# alarm limit imposed by CloudWatch.
sys_err_metrics: dict[str, cloudwatch.IMetric] = {}
for op in ("PutItem", "GetItem", "Query", "UpdateItem"):
for op in ("PutItem", "GetItem", "Query"):
sys_err_metrics[op.lower()] = cloudwatch.Metric(
namespace="AWS/DynamoDB",
metric_name="SystemErrors",
Expand Down
Loading