The golden rule: secrets never belong in source code

This should be obvious, but we still find it in production during penetration tests. API keys hardcoded in JavaScript files. Database credentials sitting in a config.py committed to a public GitHub repo. Paystack or Flutterwave secret keys embedded in mobile app bundles that anyone with a decompiler can extract in under two minutes.

The problem isn't just that the secret is visible. It's that once it's in your git history, deleting the file doesn't remove it. The secret lives in every clone, every fork, every CI cache. Rotating the credential becomes the only option, and most teams don't realise the exposure until it's too late.

Pentest finding

Hardcoded secrets in git history

In a recent engagement for a Lagos-based lending platform, we recovered production database credentials from a git commit made 14 months earlier. The file had been deleted in a subsequent commit, but the secret was still fully recoverable. The credentials were still active.

Where secrets should NOT live

Let's be explicit about what doesn't work, because each of these is something we encounter regularly during vulnerability assessments:

Approach 1: vault services

Dedicated secret management platforms are the gold standard. They centralise storage, handle encryption at rest, provide audit trails, enforce access policies, and support automatic rotation.

HashiCorp Vault

Vault provides dynamic secrets, meaning your application requests a database credential at runtime, uses it, and Vault automatically revokes it after a TTL expires. This eliminates long-lived credentials entirely. For Nigerian fintechs running on AWS or GCP, Vault integrates directly with IAM roles for authentication, so your app never handles a master secret.

Cloud-native options

AWS Secrets Manager, Google Secret Manager, and Azure Key Vault all provide managed secret storage with automatic rotation, fine-grained IAM policies, and SDK-level integration. If you're already on one of these clouds, this is the lowest-friction option. Your app fetches secrets via the SDK at startup or on-demand, and the cloud provider handles encryption, replication, and access logging.

Approach 2: CI/CD injection

If a vault feels like overkill for your current stage, CI/CD secret injection is the minimum acceptable approach. GitHub Actions Secrets, GitLab CI Variables, and Vercel Environment Variables all store encrypted values that are injected into your build or runtime environment without touching source code.

The key discipline here: scope secrets to the environment that needs them. Your staging Paystack test key should never be accessible in your production pipeline. Most CI platforms support environment-scoped secrets — use them.

Approach 3: runtime environment variables

For server-side applications, reading secrets from environment variables at runtime is a well-established pattern. Your deployment platform (Heroku, Render, Railway, or a Docker orchestrator like Kubernetes) injects the variable into the process environment. The secret never touches the filesystem.

In Kubernetes, use Secrets objects mounted as environment variables or volume files, and ensure RBAC policies restrict who can read them. Never store Kubernetes Secrets in plaintext manifests committed to git — use Sealed Secrets or integrate with an external vault.

Not sure if your secrets management is production-ready? We can audit your configuration and deployment pipeline.

Book a security review

Approach 4: the mobile backend proxy pattern

Mobile apps present a unique challenge. Unlike server-side code, anything bundled into an APK or IPA is extractable. Decompiling a Flutter, React Native, or native Android app takes minutes. Obfuscation slows an attacker down slightly — it does not prevent extraction.

The correct pattern: your mobile app authenticates the user, then calls your own backend API. Your backend holds the third-party API keys (Paystack, Mono, Flutterwave) and makes the upstream call on behalf of the mobile client. The mobile app never sees the third-party secret.

This is mandatory for any fintech handling payments or KYC/BVN data. If your mobile app directly calls a payment processor with a secret key embedded in the binary, you are one decompilation away from financial exposure.

Secret rotation and lifecycle

Storing secrets properly is half the problem. The other half is rotation. Every secret should have a defined lifetime. API keys should be rotated on a schedule — quarterly at minimum, monthly for high-value credentials. When a team member leaves, rotate every secret they had access to. No exceptions.

Automate rotation where possible. AWS Secrets Manager and Vault both support automatic rotation with zero-downtime deployment patterns. For secrets that can't be auto-rotated, maintain a runbook that your team can execute in under 30 minutes. This matters for CBN compliance and incident response readiness.

Quick reference by stack

Here's what we recommend based on where you are:

Key takeaway

Secrets management is a spectrum, not a binary

You don't need Vault on day one. But you do need to get secrets out of source code on day one. Start with CI/CD injection, graduate to a managed vault as you scale. The critical thing is that secrets are never in code, never in git history, and never shipped to clients.

Related reading

Blog: .env files in production · Can hackers see your source code? · Secure your fintech API

Guides: Fintech security checklist · OWASP for fintech · PCI DSS compliance

Services: Penetration testing · Secure architecture review · API security