Tripwire is a GitOps-native pipeline that prevents secrets from leaking into version control, automatically rotates compromised credentials, and alerts your team in real time. It layers multiple defenses — pre-commit hooks, CI scanning, Vault-managed secrets, scheduled rotation, and webhook alerts — so that a leaked secret is caught early and remediated automatically.
Status note: Event-driven and scheduled rotation entrypoints are wired end to end. The
noopprovider is always active as a safe default, and an AWS IAM provider ships credential rotation for a configured IAM user. GitHub-token and Vault-write providers are in progress.
The pipeline flows through three zones:
Developer Workstation — git commit triggers a pre-commit hook (Gitleaks + TruffleHog). If a secret is found, the commit is blocked locally.
CI/CD (GitHub Actions) — If a secret reaches the remote, CI scanning catches it, blocks the PR, and sends Discord/Slack/webhook alerts.
Infrastructure — HashiCorp Vault manages dynamic, short-lived secrets. A rotation service (cron-scheduled or event-driven) replaces compromised credentials and stores new ones in Vault for downstream consumers.
flowchart TD
A[Developer writes code] --> B[git commit]
subgraph DEV[Developer Workstation]
B --> C[pre-commit hooks]
C --> D[Gitleaks staged diff scan]
C --> E[TruffleHog entropy/verified scan]
end
D -->|secret found| F[Commit blocked locally]
E -->|secret found| F
D -->|clean| G[Commit created]
E -->|clean| G
G --> H[git push / open PR]
subgraph CI[GitHub Actions - secrets-scan workflow]
H --> I[Checkout full history fetch-depth 0]
I --> J[Gitleaks CI scan]
I --> K[TruffleHog CI scan]
J -->|findings| L[Workflow fails]
K -->|findings| L
J -->|clean| M[No finding from Gitleaks]
K -->|clean| N[No finding from TruffleHog]
M --> O[PR mergeable]
N --> O
end
L --> P[Upload SARIF findings]
L --> Q[Send Discord/Slack/webhook alert]
Q --> R[Security team notified]
P --> S[Findings visible in GitHub Security]
R --> T[Developer removes secret + rewrites history]
T --> U[Push clean commit]
U --> H
subgraph INFRA[Infrastructure - Planned/In Progress]
V[Rotation service]
W[Vault dynamic secrets]
X[Revoke compromised credentials]
Y[Issue/store replacement credentials]
end
L -. detection event .-> V
V -. rotate .-> X
V -. update .-> Y
Y -. store in .-> W
A local gate that blocks commits containing secrets before they ever reach the remote.
- Gitleaks scans staged diffs against a ruleset of regex patterns (AWS keys, GitHub tokens, private keys, etc.). Detection combines regex rules with entropy thresholds and a default allowlist of documented example values — see Understanding what gets detected (and what doesn't) for the full picture.
- TruffleHog adds entropy-based detection to catch secrets that don't match known patterns.
- Configured via a
.pre-commit-config.yamlat the repo root using the pre-commit framework. - Runs in under a second on typical diffs so it doesn't slow developers down.
When a secret is detected the commit is rejected with a message identifying the file, line, and rule that matched.
A secret must clear two independent filters before Gitleaks reports it, and knowing both is essential when you test your installation or triage a missed detection:
1. Pattern match. The value must fit a rule's regex: prefix, length, and character set (e.g. AKIA + 16 uppercase/digits for AWS access keys).
2. Entropy and allowlist checks. Even a pattern match is suppressed if the string has low Shannon entropy (repeated or sequential filler characters) or appears on the default allowlist of well-known documentation values.
We verified this against a live hook installation with three test values:
| Test value | Detected? | Reason |
|---|---|---|
AWS documented example (AKIAIOSFODNN7...) |
No | Default allowlist excludes official documentation values so tutorials and READMEs don't trigger false positives |
AKIA + repeated characters |
No | Low entropy fails the rule's threshold; it doesn't resemble a real issued key |
AKIA + random high-entropy suffix |
Yes | Matches pattern and entropy profile of a real credential |
Practical implications:
- When testing your hooks, always use a random high-entropy fake. A "test secret" copied from documentation validates nothing.
- A passing scan on files containing example values is correct behavior, not a miss.
- TruffleHog in verified-only mode reports only credentials that successfully authenticate against the real service, so fake test values will never appear in its findings. Only Gitleaks results are meaningful for synthetic tests.
- For systematic validation of the full pipeline, use the companion testbed repo (twt-repo), which plants detectable high-entropy fakes with a ground-truth manifest.
A second layer that catches anything the pre-commit hook missed — force-pushes, commits made from the web UI, or hooks that were skipped locally.
- A GitHub Actions workflow runs on every push and pull request.
- It checks out the full history (
fetch-depth: 0) and runs both Gitleaks and TruffleHog against the diff range. - If a secret is found the workflow fails, the PR is blocked from merging, and downstream jobs (rotation, alerting) are triggered.
- Results are uploaded as a SARIF artifact so findings appear directly in the GitHub Security tab.
Instead of storing long-lived credentials in config files or environment variables, Vault generates short-lived, scoped credentials on demand.
- Applications request database credentials, API keys, or cloud tokens from Vault at deploy time.
- Each credential has a TTL (e.g., 1 hour) and is automatically revoked when it expires.
- Vault's AppRole or Kubernetes auth method authenticates workloads without embedding any static secret.
- If a dynamic credential leaks, the blast radius is limited — it expires on its own and can be revoked immediately.
A rotation service that replaces compromised or aging credentials without manual intervention.
- Scheduled rotation: A cron job periodically rotates credentials (e.g., every 90 days) through Vault's rotation API or provider-specific logic (AWS
iam create-access-key, GitHub token refresh, etc.). - Event-driven rotation: When the CI scanner detects a leaked secret, it publishes an event that triggers immediate rotation of that specific credential.
- The rotation service is written in Go with a provider interface, making it easy to add new credential types.
- AWS IAM provider (
rotation/providers/aws): When a detection event's rule matches a configured prefix (defaultaws-), the provider creates a new access key for the configured IAM user, deactivates every prior key, and deletes them so the leaked credential is unusable. The newAccessKeyIdis returned in the rotation result; the plaintext secret is never logged. - Old credentials are revoked as part of the rotation to close the exposure window.
- Planned providers: a GitHub-token rotator and a Vault-write wrapper so downstream consumers pick up new credentials on their next lease renewal.
Real-time notifications so the security team knows the moment a secret is detected.
- On detection, the Go alerting package (
alerting/) sends payloads to Discord and Slack webhooks (or any HTTP endpoint). - The alert includes: repository name, branch, commit SHA, the rule that matched, the file path, and who authored the commit.
- The actual secret value is never included in the alert.
- Alerts can also be routed to PagerDuty, Opsgenie, or any incident management tool via the generic webhook sender.
Here's what happens end to end when a developer accidentally commits an AWS secret key:
| Step | What happens | Outcome |
|---|---|---|
| 1 | Developer runs git commit |
Pre-commit hook fires |
| 2 | Gitleaks scans the staged diff | Match found: AWS_SECRET_ACCESS_KEY pattern in config/settings.py |
| 3 | Commit is blocked locally | Developer sees an error with the file, line number, and matched rule |
| 4 | Developer removes the secret and commits again | Clean commit passes the hook |
| 5 | Push to remote triggers the CI workflow | GitHub Actions runs Gitleaks + TruffleHog on the push range |
| 6 | CI scan passes (secret was removed) | PR is mergeable, pipeline continues |
If the pre-commit hook was bypassed (e.g., --no-verify) and the secret reaches the remote:
| Step | What happens | Outcome |
|---|---|---|
| 5 | CI workflow detects the secret in the pushed commits | Workflow fails, PR is blocked |
| 6 | Alert sent to Discord/Slack with commit details | Security team is notified immediately |
| 7 | Rotation service receives the detection event | The compromised AWS key is rotated via iam create-access-key and the old key is deactivated |
| 8 | New credential is stored in Vault | Downstream services pick up the new key on next lease renewal |
| 9 | Developer is asked to rewrite history (git filter-repo) to scrub the secret from Git |
Exposure in version history is eliminated |
tripwire/
├── main.go # Application entrypoint
├── go.mod # Go module definition
├── .pre-commit-config.yaml # Pre-commit hook configuration
├── .gitleaks.toml # Gitleaks rules and allowlist
├── .github/
│ └── workflows/
│ └── secrets-scan.yml # CI scanning workflow
├── Dockerfile # Container build definition
├── alerting/
│ ├── alerting.go # Discord and webhook alert dispatching
│ └── alerting_test.go # Unit tests for alerting
├── cmd/
│ └── alerting/
│ └── main.go # Example alert sender entrypoint
├── k8s/ # Kubernetes manifests for local deployment
├── rotation/
│ ├── rotation.go # Rotation service and provider interface
│ ├── providers/noop/noop.go # Safe default simulated rotation provider
│ ├── providers/aws/aws.go # AWS IAM access-key rotation provider
│ └── cron.yaml # Scheduled rotation manifest scaffold
├── scripts/
│ └── run-secret-scan-testcases.sh # Local gitleaks positive/negative test harness
├── vault/
│ ├── config.hcl # Vault server config scaffold
│ ├── policies/tripwire.hcl # Tripwire policy scaffold
│ └── roles/tripwire-role.json # AppRole definition scaffold
└── README.md # This file
| Tool | Purpose | Install |
|---|---|---|
| Go | Application language | brew install go |
| pre-commit | Hook framework | pip install pre-commit |
| Gitleaks | Regex-based secret scanner | brew install gitleaks |
| TruffleHog | Entropy + regex scanner | brew install trufflehog |
| HashiCorp Vault | Secrets management | brew install vault |
GitHub CLI (gh) |
PR / Actions interaction | brew install gh |
| Docker | Container runtime | brew install --cask docker |
| Minikube | Local Kubernetes cluster | brew install minikube |
| kubectl | Kubernetes CLI | brew install kubectl |
-
Install the pre-commit hooks
pre-commit install
-
Verify hooks are working — try committing a high-entropy fake in the correct AWS access key ID format. The value must be a random-looking string, not a well-known documentation example, or the default allowlist will let it through:
echo 'AWS_ACCESS_KEY_ID=AKIA2E4XM7Q9JZ5T8B3W' > test.txt git add test.txt && git commit -m "test" # Expected: commit blocked by Gitleaks git reset HEAD~1 && rm test.txt
-
Configure Vault (requires a running Vault instance):
export VAULT_ADDR=https://vault.example.com vault login vault policy write tripwire vault/policies/tripwire.hcl -
Set up CI — push to a branch and confirm the
secrets-scanworkflow runs in the Actions tab. -
Configure alerts — add
DISCORD_WEBHOOK_URLand/orSLACK_WEBHOOK_URLsecrets to your GitHub repo settings so the workflow can send notifications. -
Configure event delivery (optional but recommended) — add
TRIPWIRE_EVENT_WEBHOOK_URLin GitHub Actions secrets, pointing to your running Tripwire endpoint:https://<your-tripwire-host>/webhook/detectionAlso add
TRIPWIRE_WEBHOOK_TOKEN; the workflow sends it as a bearer token so unauthenticated requests cannot trigger rotation or alerts. -
Run scanner test cases locally — validate your Gitleaks rules without committing test secrets:
chmod +x scripts/run-secret-scan-testcases.sh ./scripts/run-secret-scan-testcases.sh
Start the Tripwire HTTP server on your machine:
export TRIPWIRE_WEBHOOK_TOKEN='<shared-event-webhook-token>'
go run main.goThe server listens on :8080 by default. Override with the LISTEN_ADDR environment variable.
| Environment Variable | Purpose |
|---|---|
LISTEN_ADDR |
Server bind address (default :8080) |
TRIPWIRE_WEBHOOK_TOKEN |
Required bearer token for /webhook/detection |
DISCORD_WEBHOOK_URL |
Discord webhook URL for alert notifications |
SLACK_WEBHOOK_URL |
Slack webhook URL for alert notifications |
ALERT_WEBHOOK_URL |
Generic webhook endpoint for alerts |
AWS_ROTATION_IAM_USER |
Enables the AWS provider and names the IAM user whose keys are rotated. Unset = provider disabled. |
AWS_ROTATION_RULE_PREFIXES |
Comma-separated rule prefixes that trigger AWS rotation (default aws-). |
AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_PROFILE |
Standard AWS SDK credential/region variables. The provider uses the default credential chain, so any supported source (env, shared config, IMDS, IRSA) works. |
ROTATION_ENABLED |
Set to false to skip scheduled rotation runs (default true). |
Test the server:
# Health check
curl http://localhost:8080/healthz
# Simulate a secret detection event
curl -X POST http://localhost:8080/webhook/detection \
-H "Authorization: Bearer $TRIPWIRE_WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"repository": "myorg/myapp",
"branch": "main",
"commit_sha": "abc1234",
"rule": "aws-access-key",
"file_path": "config/settings.py",
"author": "dev@example.com"
}'-
Start Docker and Minikube
open -a Docker # macOS — start Docker Desktop minikube start -
Build the container image inside Minikube's Docker
eval $(minikube docker-env) docker build -t tripwire:latest .
-
Create the Kubernetes secret with your credentials:
kubectl create secret generic tripwire-secrets \ --from-literal=TRIPWIRE_WEBHOOK_TOKEN='<shared-event-webhook-token>' \ --from-literal=DISCORD_WEBHOOK_URL='<your-discord-webhook-url>' \ --from-literal=VAULT_TOKEN='<your-vault-token>'
-
Apply the manifests
kubectl apply -f k8s/configmap.yaml \ -f k8s/deployment.yaml \ -f k8s/service.yaml -
Verify the deployment
kubectl get pods -l app=tripwire kubectl logs -l app=tripwire
-
Access the service via port-forward:
kubectl port-forward svc/tripwire 8080:8080
Then test with the same
curlcommands from the local setup above. -
Stop the cluster when done:
minikube stop
go test ./...MIT
Mirabel Vuong and Andrew Duong