{"id":1791,"date":"2025-11-13T19:33:51","date_gmt":"2025-11-13T16:33:51","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/the-calm-way-to-secrets-on-a-vps-gitops-with-sops-age-systemd-magic-and-rotation-you-can-sleep-on\/"},"modified":"2025-11-13T19:33:51","modified_gmt":"2025-11-13T16:33:51","slug":"the-calm-way-to-secrets-on-a-vps-gitops-with-sops-age-systemd-magic-and-rotation-you-can-sleep-on","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/the-calm-way-to-secrets-on-a-vps-gitops-with-sops-age-systemd-magic-and-rotation-you-can-sleep-on\/","title":{"rendered":"The Calm Way to Secrets on a VPS: GitOps with sops + age, systemd Magic, and Rotation You Can Sleep On"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#So_Here8217s_the_Secret_About_Secrets\"><span class=\"toc_number toc_depth_1\">1<\/span> So, Here&#8217;s the Secret About Secrets<\/a><\/li><li><a href=\"#Why_GitOps_sops_age_Feels_Like_a_Breath_of_Fresh_Air\"><span class=\"toc_number toc_depth_1\">2<\/span> Why GitOps + sops + age Feels Like a Breath of Fresh Air<\/a><\/li><li><a href=\"#Bootstrapping_the_VPS_Keys_Rules_and_a_Calm_First_Commit\"><span class=\"toc_number toc_depth_1\">3<\/span> Bootstrapping the VPS: Keys, Rules, and a Calm First Commit<\/a><ul><li><a href=\"#Generating_and_placing_the_age_key\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Generating and placing the age key<\/a><\/li><li><a href=\"#Teaching_sops_your_intentions_with_sopsyaml\"><span class=\"toc_number toc_depth_2\">3.2<\/span> Teaching sops your intentions with .sops.yaml<\/a><\/li><li><a href=\"#Writing_your_first_encrypted_file\"><span class=\"toc_number toc_depth_2\">3.3<\/span> Writing your first encrypted file<\/a><\/li><li><a href=\"#Repo_hygiene_so_you_dont_shoot_your_foot\"><span class=\"toc_number toc_depth_2\">3.4<\/span> Repo hygiene so you don\u2019t shoot your foot<\/a><\/li><\/ul><\/li><li><a href=\"#Decryption_at_Runtime_with_systemd_Ephemeral_and_Predictable\"><span class=\"toc_number toc_depth_1\">4<\/span> Decryption at Runtime with systemd: Ephemeral and Predictable<\/a><ul><li><a href=\"#One_more_layer_crash-safe_decrypts\"><span class=\"toc_number toc_depth_2\">4.1<\/span> One more layer: crash-safe decrypts<\/a><\/li><\/ul><\/li><li><a href=\"#Living_the_GitOps_Life_Editing_Reviewing_and_Deploying_Secrets\"><span class=\"toc_number toc_depth_1\">5<\/span> Living the GitOps Life: Editing, Reviewing, and Deploying Secrets<\/a><\/li><li><a href=\"#Safe_Rotation_Without_the_Drama\"><span class=\"toc_number toc_depth_1\">6<\/span> Safe Rotation Without the Drama<\/a><\/li><li><a href=\"#Reloads_That_Respect_Users_systemdpath_Oneshot_Services\"><span class=\"toc_number toc_depth_1\">7<\/span> Reloads That Respect Users: systemd.path + Oneshot Services<\/a><\/li><li><a href=\"#Little_Habits_That_Make_This_Rock-Solid\"><span class=\"toc_number toc_depth_1\">8<\/span> Little Habits That Make This Rock-Solid<\/a><ul><li><a href=\"#Keep_decrypted_material_ephemeral\"><span class=\"toc_number toc_depth_2\">8.1<\/span> Keep decrypted material ephemeral<\/a><\/li><li><a href=\"#Audit_who_can_decrypt\"><span class=\"toc_number toc_depth_2\">8.2<\/span> Audit who can decrypt<\/a><\/li><li><a href=\"#Practice_rotation_on_staging\"><span class=\"toc_number toc_depth_2\">8.3<\/span> Practice rotation on staging<\/a><\/li><li><a href=\"#Dont_leak_in_CI_logs\"><span class=\"toc_number toc_depth_2\">8.4<\/span> Don\u2019t leak in CI logs<\/a><\/li><li><a href=\"#Know_when_to_restart_vs_reload\"><span class=\"toc_number toc_depth_2\">8.5<\/span> Know when to restart vs reload<\/a><\/li><\/ul><\/li><li><a href=\"#A_Real-World_Story_The_Day_a_Token_Went_Bad\"><span class=\"toc_number toc_depth_1\">9<\/span> A Real-World Story: The Day a Token Went Bad<\/a><\/li><li><a href=\"#Edge_Cases_Gotchas_and_the_Quiet_Wisdom\"><span class=\"toc_number toc_depth_1\">10<\/span> Edge Cases, Gotchas, and the Quiet Wisdom<\/a><\/li><li><a href=\"#Putting_It_All_Together_and_Sleeping_Better\"><span class=\"toc_number toc_depth_1\">11<\/span> Putting It All Together (and Sleeping Better)<\/a><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"So_Here8217s_the_Secret_About_Secrets\">So, Here&#8217;s the Secret About Secrets<\/span><\/h2>\n<p>Ever had that moment when you SSH into a <a href=\"https:\/\/www.dchost.com\/vps\">VPS<\/a>, peek at an old <strong>.env<\/strong> file, and instantly feel your shoulders tighten because you have no idea who last touched it, whether it leaked into a backup, or if the values even match production? I\u2019ve been there. A few years back, I walked into a client\u2019s environment where each server had its own shadow copy of secrets, half of them outdated, and nobody was brave enough to rotate anything. The team moved fast, the app shipped features, but the secrets\u2014API keys, database passwords, webhook tokens\u2014were like dusty skeletons in the closet.<\/p>\n<p>That\u2019s when I started leaning hard into GitOps and found a workflow that finally felt sane: <strong>sops + age<\/strong> for encryption in Git, with a tiny sprinkle of <strong>systemd<\/strong> for runtime decryption and safe reloads. It wasn\u2019t magic, but it felt like it. No more guessing, no more strings stuffed into CI variables that silently rot, no more \u201cplease don\u2019t cat that file on prod.\u201d Just a friendly, predictable flow that lives alongside your code and deploys like any other artifact.<\/p>\n<p>In this post, I\u2019ll show you exactly how I manage secrets on a VPS with sops and age, how I wire that into systemd so services get the right environment at the right time, and how I rotate without breaking anything\u2014or losing sleep. We\u2019ll talk practical repo layout, first-boot bootstrapping, reloads that don\u2019t cause downtime, and a rotation playbook that actually works. By the end, you\u2019ll have a quiet confidence about secrets again.<\/p>\n<h2 id=\"section-2\"><span id=\"Why_GitOps_sops_age_Feels_Like_a_Breath_of_Fresh_Air\">Why GitOps + sops + age Feels Like a Breath of Fresh Air<\/span><\/h2>\n<p>Here\u2019s the thing: people love Git because it tells the story of change. Secrets need that story too, but they also need to stay secret. <strong>sops<\/strong> gives us that sweet spot. It stores encrypted values right inside YAML or JSON, and it does it in a way that plays nicely with Git diffs, reviews, and branches. And the encryption part? That\u2019s where <strong>age<\/strong> shines\u2014simple, modern, and fast. The first time I replaced a gnarly GPG setup with age recipients, I felt like someone took training wheels off my workflow. Keys were just\u2026 readable, rotate-able, understandable.<\/p>\n<p>What I love most is the flow: developers can open a pull request with updated secrets, reviewers can see exactly what changed (encrypted, yes, but key placement and structure still readable), and the server decrypts at runtime using its own private key. That last part is important\u2014we never ship the secret in plain text over CI or stash it in some shared bucket that turns into a quiet liability. The VPS itself is the one with the key. The repository stays safe to clone. And if you lose the server? Rotate the recipient, reencrypt, redeploy, and move on.<\/p>\n<p>For installs and docs, I like to keep the official resources handy. If you haven\u2019t met these tools yet, take a quick skim of <a href=\"https:\/\/github.com\/getsops\/sops\" rel=\"nofollow noopener\" target=\"_blank\">sops on GitHub<\/a> and the lovely <a href=\"https:\/\/github.com\/FiloSottile\/age\" rel=\"nofollow noopener\" target=\"_blank\">age project<\/a>. And for the glue that keeps the runtime neat, the <a href=\"https:\/\/www.freedesktop.org\/software\/systemd\/man\/latest\/systemd.unit.html\" rel=\"nofollow noopener\" target=\"_blank\">systemd documentation<\/a> is worth bookmarking. We\u2019ll lean on systemd just enough to keep decrypted secrets ephemeral and reloads predictable.<\/p>\n<h2 id=\"section-3\"><span id=\"Bootstrapping_the_VPS_Keys_Rules_and_a_Calm_First_Commit\">Bootstrapping the VPS: Keys, Rules, and a Calm First Commit<\/span><\/h2>\n<h3><span id=\"Generating_and_placing_the_age_key\">Generating and placing the age key<\/span><\/h3>\n<p>I keep decryption on the server. That means the VPS needs its own age keypair. On first boot, I\u2019ll log in and create it:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo mkdir -p \/etc\/age\nsudo age-keygen -o \/etc\/age\/key.txt\nsudo chmod 600 \/etc\/age\/key.txt\nsudo chown root:root \/etc\/age\/key.txt\n<\/code><\/pre>\n<p>Age key files are just short and friendly. Open the file and you\u2019ll see a public key line starting with <strong>age1<\/strong>. That\u2019s the recipient you add to your repo so sops can encrypt for this VPS. I usually export the path for convenience:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">export SOPS_AGE_KEY_FILE=\/etc\/age\/key.txt\n<\/code><\/pre>\n<p>If you automate your VPS builds (and I think you should), you can bake this step into first-boot user-data or config management. I covered a similar first-boot approach in my write-up on <a href=\"https:\/\/www.dchost.com\/blog\/en\/bulutun-ilk-nefesi-cloud%E2%80%91init-ve-ansible-ile-tekrar-uretilebilir-vps-nasil-kurulur\/\">how I bootstrap a VPS with cloud\u2011init and Ansible<\/a>. The exact dance is up to you\u2014what matters is the private key only lives on the node that needs to decrypt.<\/p>\n<h3><span id=\"Teaching_sops_your_intentions_with_sopsyaml\">Teaching sops your intentions with .sops.yaml<\/span><\/h3>\n<p>In the Git repo, I add a <strong>.sops.yaml<\/strong> file that spells out who can decrypt which files. I like creation rules with clear patterns, so future-me doesn\u2019t forget how secrets flow:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">creation_rules:\n  - path_regex: secrets\/.*.yaml$\n    encrypted_regex: '^(data|secrets)$'\n    age: [&quot;age1examplepublickeyfor-prod&quot;, &quot;age1examplepublickeyfor-staging&quot;]\n  - path_regex: secrets\/.*.env.sops$\n    age: [&quot;age1examplepublickeyfor-prod&quot;]\n<\/code><\/pre>\n<p>You can mix and match patterns by environment. For example: staging files encrypt to the staging recipient, production to production. If you have multiple servers in a cluster, you can add multiple recipients. sops will encrypt the same value for each recipient, and <em>any<\/em> one of the matching private keys can decrypt. It\u2019s like having multiple copies of the same safe combination, each sealed for a specific person.<\/p>\n<h3><span id=\"Writing_your_first_encrypted_file\">Writing your first encrypted file<\/span><\/h3>\n<p>I usually start with a simple <strong>secrets\/app.env.sops<\/strong> file. It can be a plain env file with key=value lines, or a tidy YAML blob\u2014use what your services are happiest to consume. Let\u2019s say it\u2019s an env file:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">DB_HOST=10.10.0.5\nDB_USER=app\nDB_PASS=please-rotate-me\nAPI_TOKEN=super-secret\n<\/code><\/pre>\n<p>Now encrypt it:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sops --encrypt --in-place secrets\/app.env.sops\n<\/code><\/pre>\n<p>And in Git, commit the encrypted file. It\u2019ll look like a block of structured ciphertext. One of the best moments is when you realize that <strong>everything secret<\/strong> is now inside your repo, but still safe. No more DMing credentials. No more mystery variables.<\/p>\n<h3><span id=\"Repo_hygiene_so_you_dont_shoot_your_foot\">Repo hygiene so you don\u2019t shoot your foot<\/span><\/h3>\n<p>I always add basic protections:<\/p>\n<p>&#8211; A strict <strong>.gitignore<\/strong> that rejects any decrypted paths like <strong>secrets\/*.env<\/strong> or <strong>secrets\/*.dec<\/strong> or temporary files in <strong>\/run<\/strong>.<br \/>&#8211; Keep decrypted artifacts out of backups. If you write to <strong>\/run<\/strong>, you\u2019re already in good shape because it\u2019s a tmpfs.<br \/>&#8211; Consider a pre-commit hook that barks if a plaintext .env slips in. Friendly barks save days.<\/p>\n<h2 id=\"section-4\"><span id=\"Decryption_at_Runtime_with_systemd_Ephemeral_and_Predictable\">Decryption at Runtime with systemd: Ephemeral and Predictable<\/span><\/h2>\n<p>Once your repo lands on the VPS, you want services to get fresh secrets at start and during rotation, without leaving decrypted files on disk. This is where systemd quietly shines. I like to decrypt into <strong>\/run<\/strong>\u2014it\u2019s memory-backed and disappears on reboot. Then I point <strong>EnvironmentFile<\/strong> at that decrypted file.<\/p>\n<p>Here\u2019s a minimal service that does exactly that:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">[Unit]\nDescription=My App\nAfter=network.target\n\n[Service]\nType=simple\nEnvironmentFile=\/run\/myapp\/app.env\nExecStartPre=\/usr\/bin\/mkdir -p \/run\/myapp\nExecStartPre=\/usr\/bin\/sops --decrypt \n  --output \/run\/myapp\/app.env \n  \/srv\/myrepo\/secrets\/app.env.sops\nExecStart=\/usr\/local\/bin\/myapp\nUser=myapp\nGroup=myapp\n# Hardening bits you can layer on as needed\nNoNewPrivileges=yes\nPrivateTmp=yes\nProtectSystem=full\nProtectHome=true\n\n[Install]\nWantedBy=multi-user.target\n<\/code><\/pre>\n<p>Two things I\u2019ve learned: keep permissions tight on the decrypted file (either via chmod in an ExecStartPre step or by using a specific user for your service), and don\u2019t log its contents by accident. Your logs should be helpful, not explosive.<\/p>\n<p>If your app prefers YAML or JSON instead of env files, you can decrypt straight to that format and pass a different flag to your app. The trick is the same: decrypt to <strong>\/run<\/strong>, then point your process at it. And if you need a short-lived process to read secrets and exit, consider <strong>sops exec-env<\/strong> to run a command with secrets injected temporarily\u2014you\u2019ll appreciate how clean it feels for one-off tasks.<\/p>\n<h3><span id=\"One_more_layer_crash-safe_decrypts\">One more layer: crash-safe decrypts<\/span><\/h3>\n<p>Every once in a while, with lots of restarts flying around, I\u2019ll add a tiny lock so decrypts don\u2019t race each other. It\u2019s not strictly required, but it keeps journals less noisy:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">ExecStartPre=\/usr\/bin\/flock -n \/run\/myapp\/.sops.lock \n  \/usr\/bin\/sops --decrypt --output \/run\/myapp\/app.env \/srv\/myrepo\/secrets\/app.env.sops\n<\/code><\/pre>\n<p>It\u2019s a small courtesy to your future self during busy deploys.<\/p>\n<h2 id=\"section-5\"><span id=\"Living_the_GitOps_Life_Editing_Reviewing_and_Deploying_Secrets\">Living the GitOps Life: Editing, Reviewing, and Deploying Secrets<\/span><\/h2>\n<p>The happy path looks like this: a teammate needs to update a token. They open the encrypted file with sops (which handles editing with your normal $EDITOR), change the value, save, and commit. The PR shows a clean structural diff where it matters, and reviewers know exactly what was changed and why.<\/p>\n<p>On your CI, you do not decrypt. That\u2019s the point. CI should lint, build, test, and ship artifacts\u2014but secrets stay encrypted end-to-end until they reach the node that owns the key. Your deploy step brings the repo to the VPS (or pulls, depending on your strategy), and systemd does the rest when the service restarts. If you\u2019re using configuration management like Ansible, the <em>community.sops<\/em> plugin can decrypt on the node too, as long as the age key is present there. It blends well with the same philosophy: decrypt where you consume.<\/p>\n<p>In my experience, the biggest win isn\u2019t even security\u2014it\u2019s <strong>clarity<\/strong>. You stop guessing where secrets live and which ones are current. It\u2019s all in Git, with a story and a timeline you can trust.<\/p>\n<h2 id=\"section-6\"><span id=\"Safe_Rotation_Without_the_Drama\">Safe Rotation Without the Drama<\/span><\/h2>\n<p>Rotation used to terrify me. Then I started treating it like a deploy with a steady cadence. The safest way I\u2019ve found looks like a short, two-step dance with sops and age recipients:<\/p>\n<p>First, you add the new recipient (the new public key) to your <strong>.sops.yaml<\/strong> rules. Then re-encrypt affected files so they include both the old and the new recipients. Now both parties can decrypt. Deploy that. Confirm services still hum on all nodes. Only then, remove the old recipient and re-encrypt again. Deploy the removal. That\u2019s it\u2014two moves, no cliff.<\/p>\n<p>In practice, it looks like this:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Step 1: add the new recipient\n# .sops.yaml: age: [old, new]\n# Then re-encrypt\nsops --encrypt --in-place secrets\/app.env.sops  # or: sops updatekeys -r\n\n# Deploy and verify decrypt works everywhere\n\n# Step 2: remove the old recipient\n# .sops.yaml: age: [new]\n# Re-encrypt again\nsops --encrypt --in-place secrets\/app.env.sops  # or: sops updatekeys -r\n\n# Deploy and verify again\n<\/code><\/pre>\n<p>When rotating individual secret <em>values<\/em> (like replacing DB credentials), I tend to duplicate the variable into a new key and let the app read from both for a short window. Think of it like changing the lock on your front door while leaving the old key working for a day. Once everything\u2019s moved over, delete the old variable and push a final clean-up.<\/p>\n<p>One more practical tip: if your app supports hot reloads for credentials, wire that into systemd so a rotation is a reload, not a restart. You can orchestrate a graceful handoff where no user ever notices.<\/p>\n<h2 id=\"section-7\"><span id=\"Reloads_That_Respect_Users_systemdpath_Oneshot_Services\">Reloads That Respect Users: systemd.path + Oneshot Services<\/span><\/h2>\n<p>Rotations shouldn\u2019t feel like pulling the plug. I like pairing a <strong>Path<\/strong> unit with a tiny oneshot service that re-decrypts and tells the app to reload. The flow is simple: whenever the encrypted file changes in Git and reaches the server, systemd sees the change, runs the decrypt, and triggers a reload. The main service never fully restarts unless you want it to.<\/p>\n<p>Here\u2019s a pattern that\u2019s treated me well:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># \/etc\/systemd\/system\/myapp-reload.service\n[Unit]\nDescription=Re-decrypt secrets and reload My App\n\n[Service]\nType=oneshot\nExecStart=\/usr\/bin\/sops --decrypt --output \/run\/myapp\/app.env \/srv\/myrepo\/secrets\/app.env.sops\nExecStart=\/bin\/systemctl reload myapp.service\n<\/code><\/pre>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># \/etc\/systemd\/system\/myapp.path\n[Unit]\nDescription=Watch encrypted secrets for My App\n\n[Path]\nPathChanged=\/srv\/myrepo\/secrets\/app.env.sops\nUnit=myapp-reload.service\n\n[Install]\nWantedBy=multi-user.target\n<\/code><\/pre>\n<p>Enable both the main service and the path unit, and you\u2019ve got a self-healing loop for secrets. It\u2019s quietly elegant. When you merge a PR with a rotated token, the node decrypts and smoothly reloads without a blip.<\/p>\n<h2 id=\"section-8\"><span id=\"Little_Habits_That_Make_This_Rock-Solid\">Little Habits That Make This Rock-Solid<\/span><\/h2>\n<h3><span id=\"Keep_decrypted_material_ephemeral\">Keep decrypted material ephemeral<\/span><\/h3>\n<p>Always write decrypted files to <strong>\/run<\/strong>. If you must write elsewhere, consider tmpfs mounts or cleanup hooks. And make sure systemd hardening options don\u2019t block your app from reading the file\u2014tight but thoughtful permissions are the game.<\/p>\n<h3><span id=\"Audit_who_can_decrypt\">Audit who can decrypt<\/span><\/h3>\n<p>As your team grows, it\u2019s tempting to add every machine\u2019s recipient to every file. Resist. Keep <strong>.sops.yaml<\/strong> rules specific, and embrace per-environment or per-role files so a node only decrypts what it needs. You\u2019ll thank yourself during incident reviews.<\/p>\n<h3><span id=\"Practice_rotation_on_staging\">Practice rotation on staging<\/span><\/h3>\n<p>Do one dry run start-to-finish: add recipient, re-encrypt, deploy, remove recipient, re-encrypt, deploy. Practice the ritual so production is just muscle memory. While you\u2019re at it, rotate a few secret values and confirm your reload path works just as expected.<\/p>\n<h3><span id=\"Dont_leak_in_CI_logs\">Don\u2019t leak in CI logs<\/span><\/h3>\n<p>Even if you never decrypt in CI, some build tools get chatty. Make sure no step tries to print decrypted content. If you have custom scripts, silence them on sensitive paths. Your future self, far from a pager, will appreciate the peace.<\/p>\n<h3><span id=\"Know_when_to_restart_vs_reload\">Know when to restart vs reload<\/span><\/h3>\n<p>Some apps read credentials only on startup. If that\u2019s your world, pair decrypt + restart, but do it with grace\u2014drain traffic, then restart, then un-drain. If your app supports live reloads, treat yourself to that smoother path and wire systemd accordingly.<\/p>\n<h2 id=\"section-9\"><span id=\"A_Real-World_Story_The_Day_a_Token_Went_Bad\">A Real-World Story: The Day a Token Went Bad<\/span><\/h2>\n<p>One afternoon, an integration token got revoked on a client\u2019s stack. It was the kind of moment that used to turn into a firefight: who has the token, who knows where it lives, how do we deploy it without breaking everything else? This time, the fix was almost boring.<\/p>\n<p>We generated a new token, opened the encrypted file with sops, replaced the value, and pushed a PR. Another engineer reviewed it, we merged, the repo sync hit the VPS, and the <strong>systemd.path<\/strong> unit triggered our oneshot reload. The app kept running while it reloaded the config. The error counters flattened. No emergency call. No \u201cwho has permissions for that CI variable?\u201d scramble. It felt odd in the best way\u2014like discovering a new door to an old room that\u2019s been there all along.<\/p>\n<h2 id=\"section-10\"><span id=\"Edge_Cases_Gotchas_and_the_Quiet_Wisdom\">Edge Cases, Gotchas, and the Quiet Wisdom<\/span><\/h2>\n<p>There are always little corners to watch out for. If you run multiple apps on the same node, keep secrets separated by directory and permissions\u2014don\u2019t let one service read another\u2019s decrypted files just because they\u2019re neighbors. If you back up the box, double-check you\u2019re not accidentally capturing <strong>\/run<\/strong>. And if you mirror the repo to other places, make sure write access stays tight\u2014encrypted or not, Git hygiene matters.<\/p>\n<p>Another gentle reminder: if you ever need to move a server or rebuild it, that private key in <strong>\/etc\/age\/key.txt<\/strong> is the crown jewel. Decide whether you\u2019ll migrate it to the new node or rotate recipients and re-encrypt. Both approaches are fine\u2014what matters is that you do it intentionally, with the same calm steps you practiced in staging.<\/p>\n<p>Finally, let your team share in the ritual. A short readme in the repo that explains how to edit secrets with sops, how rotation works, and how services pick up changes, removes so much mystery. The fewer \u201csecret keepers\u201d you have, the safer your stack feels.<\/p>\n<h2 id=\"section-11\"><span id=\"Putting_It_All_Together_and_Sleeping_Better\">Putting It All Together (and Sleeping Better)<\/span><\/h2>\n<p>If we zoom out, the flow looks beautifully simple:<\/p>\n<p>1) The VPS owns an <strong>age<\/strong> private key. Its public half lives in <strong>.sops.yaml<\/strong> as a recipient.<br \/>2) Secrets live in Git as encrypted files, edited with <strong>sops<\/strong>, reviewed like normal code.<br \/>3) Deploy brings the repo to the server. <strong>systemd<\/strong> decrypts to <strong>\/run<\/strong> at start or on change, and your app reads a clean, ephemeral file.<br \/>4) Rotation is just two pull requests with a grace period between them\u2014add recipient, then remove the old one. Or, for values, switch keys in app config and later prune the old ones.<\/p>\n<p>That\u2019s the calm path. You trade fear for a routine. And quietly, that routine raises your bar\u2014fewer secrets drifting through chat, no plaintext accidents on disk, and a clear trail of who changed what and when.<\/p>\n<p>If you\u2019re starting fresh, try it on a smaller service first. Add a single encrypted env file, wire a systemd unit that decrypts to <strong>\/run<\/strong>, and practice the reload dance. Once it clicks, roll it across the stack. And if you\u2019re automating first boot, tuck the age key generation right into your provisioner so new nodes are born ready.<\/p>\n<p>Hope this was helpful. If you want to layer this into a larger automation story, I\u2019ve had a lot of fun pairing it with cloud-init and configuration management\u2014the same calm approach extends nicely. Until next time, keep your secrets tidy and your reloads boring, because boring is quietly beautiful in production.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>\u0130&ccedil;indekiler1 So, Here&#8217;s the Secret About Secrets2 Why GitOps + sops + age Feels Like a Breath of Fresh Air3 Bootstrapping the VPS: Keys, Rules, and a Calm First Commit3.1 Generating and placing the age key3.2 Teaching sops your intentions with .sops.yaml3.3 Writing your first encrypted file3.4 Repo hygiene so you don\u2019t shoot your foot4 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1792,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1791","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1791","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1791"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1791\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1792"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1791"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1791"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1791"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}