Secrets Management: A Practical Guide from Cloud Native to GitOps
Effective secrets management is the discipline of controlling access to the digital keys — API tokens, database passwords and private…
Secrets Management: A Practical Guide from Cloud Native to GitOps

Effective secrets management is the discipline of controlling access to the digital keys — API tokens, database passwords and private certificates — that unlock your infrastructure. As organizations adopt modern DevOps practices, the “old ways” of sharing .env files over chat or hardcoding passwords are no longer just bad practice; they are a critical vulnerability.
This guide provides a comprehensive strategy. We first explore the Cloud-Native approach (using AWS/Azure/GCP native vaults) and then detail the GitOps approach (managing encrypted secrets in code repositories), concluding with a hands-on technical walkthrough.
The Cloud-Native Approach (AWS, Azure, GCP)
If you prefer not to manage encryption keys yourself, you can use the managed vault provided by your cloud vendor. In this model, secrets are never stored in your code repository. Instead, they live in a secure, centralized vault and are injected into your application at runtime.
The workflow is uniform across all major providers: Store the secret in the vault, Authorize your application’s identity (IAM) to read it and Fetch it at runtime.
1. Storage (Creating the Secret)
Instead of writing a password into a file, you push it securely to the cloud provider’s vault using their CLI or Console.
- AWS Secrets Manager:
aws secretsmanager create-secret --name production/db_pass --secret-string "CorrectHorseBatteryStaple"
- Google Secret Manager:
printf "CorrectHorseBatteryStaple" | gcloud secrets create production-db-pass --data-file=-
- Azure Key Vault:
az keyvault secret set --vault-name "MyProductionVault" --name "DbPassword" --value "CorrectHorseBatteryStaple"
2. Access (The “Secret Zero” Solution)
The most powerful feature of cloud vaults is Identity Access Management (IAM). You do not need a password to access the password.
- On AWS: You assign an IAM Role to your EC2 instance or Lambda function.
- On Azure: You assign a Managed Identity to your VM or App Service.
- On GCP: You assign a Service Account to your Cloud Run or GKE workload.
You then create a policy that says: “Only this specific Role/Identity can read the production/db_pass secret."
3. Retrieval (Runtime Injection)
Your application retrieves the secret in one of two ways:
- SDK Retrieval: Your code uses the cloud SDK (e.g.,
boto3for Python) to call the vault API on startup. - Environment Injection: Container orchestrators (like ECS or Kubernetes) can fetch the secret and inject it as an environment variable (
DB_PASS) when the container starts. The app sees a standard environment variable, unaware it came from a vault.
The GitOps Strategy (Age & PGP)
Many teams prefer to keep all configuration, including secrets, in their Git repository. This ensures that the “state” of the infrastructure is versioned and audit-able. However, you cannot commit plain text secrets.
The Tooling: Age vs. PGP
To store secrets in Git, we must encrypt the file.
- PGP (Pretty Good Privacy): The legacy standard. While powerful (supporting signing, web-of-trust), it is complex to automate and prone to configuration errors.
- Age (Actually Good Encryption): The modern standard. It is designed specifically for file encryption. It uses small, explicit keys (just text strings), has no config files, and is much easier to use in CI/CD pipelines.
Verdict: For modern development, Age combined with SOPS (Secrets OPerationS) is the industry-standard recommendation.
Technical Playbook (The “Secret-Ops” Flow)
This section details the “SOPS + Age” workflow covering the lifecycle: Local Development, Secure Backup in Git and Production Deployment.
Phase 1: Local Development
Goal: Keep secrets working on local computer but out of the Git history.
Step 1: Create the files In our project root, create two files:
.env(This holds our real local secrets)..env.example(This holds fake/empty values for documentation).
Step 2: Ignore the real file Open (or create) our .gitignore file and add this immediately to prevent accidents:
.env
.env.local
*.enc.env
Step 3: Fill them out
- .env:
DB_PASS=correct_password_123 - .env.example:
DB_PASS=
Phase 2: Storing in Git (Encrypted)
Goal: Commit secrets to Git so we have a backup, but encrypt them so no one (not even Git Providers) can read them.
1. Install Tools (On local machine)
Age (The encryption backend):
## Install Age
# macOS
brew install age
# Debian/Ubuntu
sudo apt update && sudo apt install age
SOPS (The editor): Download the latest binary or .deb from the(https://github.com/getsops/sops/releases).
# Download the binary
curl -LO https://github.com/getsops/sops/releases/download/v3.11.0/sops-v3.11.0.linux.amd64
# Move the binary in to our PATH
mv sops-v3.11.0.linux.amd64 /usr/local/bin/sops
# Make the binary executable
chmod +x /usr/local/bin/sops
2. Key Generation: Generate a key file. Keep this file safe! If we lose it, we lose our encrypted data.
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
Output Example: # public key: age1... AGE-SECRET-KEY-1...
Note: This creates a private key on disk. We will need to copy this file to other computers or servers to decrypt data there.
3. Configure the Project: Create a file named .sops.yaml in project root. This tells SOPS which key to use for this specific project.
#.sops.yaml
creation_rules:
- path_regex: \.env$
key_groups:
- age:
- "age1ql3z7hjy54pw3..." # COPY THE PUBLIC KEY HERE (from keys.txt)
4. Encrypt and Commit: Now, we can encrypt our local .env file into a new file that is safe to commit.
sops --encrypt .env >.env.encrypted
We can now securely run:
git add .env.encrypted .sops.yaml
git commit -m "Add encrypted production secrets"
git push
Result: The file in Git looks like random garbage text, protecting our data.
5. Editing Secrets Later: To update secrets, we do not need to decrypt manually but edit the encrypted file directly using SOPS:
sops .env.encrypted
This opens the file in our default editor (vim/nano/…), shows it decrypted and re-encrypts it automatically when we save and close.
Phase 3: Production Server Setup (Debian/Ubuntu)
Goal: Get the secrets onto the server and readable ONLY by our application.
Method A: The Secure File (Recommended for PHP/Web Apps)
1. Transfer the Key (One-time setup): We need the keys.txt generated in Phase 2 on our server.
- Manual: Copy the content of
~/.config/sops/age/keys.txtfrom our PC. - Server: Paste it into the same location:
~/.config/sops/age/keys.txt.
2. Decrypt on Server: Install sops and age on the server (same steps as Phase 2). Then, navigate to project folder and run:
sops --decrypt .env.encrypted >.env
Now we have the real .env file on server.
3. Lock Down Permissions (Critical Step): We don’t want other users on the server reading the file. Assuming we are using Nginx/Apache (which usually runs as www-data):
# 1. Set the owner to current user and the group to the Web Server
sudo chown $USER:www-data .env
# 2. Set permissions:
# - User ($USER): Read & Write (6)
# - Group (www-data): Read Only (4)
# - Others: No Access (0)
sudo chmod 640 .env
4. Verification: Test if it’s secure by trying to read it as a different user:
# Output should be: Permission denied (trying with example user: nobody)
sudo -u nobody cat.env
Method B: systemd Override (Recommended for Daemons/Workers)
If we are running a background worker (like a Laravel Queue, Node.js or Python script) managed by systemd, placing a file is less secure than injecting it directly into the process environment.
1. Edit the service configuration
sudo systemctl edit <our-service-name>
2. Add variables Add this block. Note that systemctl edit creates an override file, so it won't be overwritten by updates.
Environment="DB_PASSWORD=production_password_here"
Environment="API_KEY=another_secret"
Tip: If we have many variables, can use EnvironmentFile=/path/to/locked/down/.env instead.
3. Reload
sudo systemctl daemon-reload
sudo systemctl restart <our-service-name>
Phase 4: Emergency Response (Scrubbing Git History)
Accidentally committed a plain text .env file (not the encrypted one)? Simply deleting it, is not enough. It still lives in the .git history folder.
The Fix: Use git-filter-repo (The modern replacement for BFG/filter-branch).
- Rotate: Change the passwords immediately. The old ones are compromised.
- Install:
pip install git-filter-repo - Scrub:
git filter-repo — invert-paths — path .env - Force Push:
git push --force --all
Warning: This is destructive. All team members must re-clone the repository after this step.
Summary of Daily Workflow
Once setup is complete, team’s daily workflow looks like this:
Change a secret locally:
- Edit
.envfor our local app to work. - Run
sops .env.encryptedand add the new key/value there too. git commit .env.encrypted.
Deploy:
git pullon server.- Run
sops --decrypt .env.encrypted >.env. - Restart web server (e.g.,
sudo systemctl reload php8.x-fpm).
Conclusion
Secrets management is not a “set it and forget it” task; it is an active operational discipline. By choosing one of the two paths outlined in this guide — Cloud Native for pure infrastructure teams or GitOps with Age for code-centric teams — you eliminate the most common attack vectors.
Whether you rely on Secrets Manager or an encrypted .env file, the golden rule remains the same: Plain text secrets must never enter your version control system. Implementing the workflows above will ensure your organization moves from "security by obscurity" to a true Zero Trust posture.
메타데이터
- post_id
- 3a9c9c4072d0
- slug
- secrets-management-a-practical-guide-from-cloud-native-to-gitops-3a9c9c4072d0
- url
- https://blog.infocyph.com/secrets-management-a-practical-guide-from-cloud-native-to-gitops-3a9c9c4072d0
- canonical_url
- https://blog.infocyph.com/secrets-management-a-practical-guide-from-cloud-native-to-gitops-3a9c9c4072d0
- author_url
- https://medium.com/@abmmhasan
- status
- ok
- fetched_at
- 2026-06-23 06:34:20