[{"data":1,"prerenderedAt":361},["ShallowReactive",2],{"blog-stories/github-secrets-public":3},{"id":4,"title":5,"body":6,"category":341,"date":342,"dateModified":342,"description":343,"draft":344,"extension":345,"faq":346,"featured":344,"headerVariant":341,"image":346,"keywords":346,"meta":347,"navigation":348,"ogDescription":349,"ogTitle":346,"path":350,"readTime":351,"schemaOrg":352,"schemaType":353,"seo":354,"sitemap":355,"stem":356,"tags":357,"twitterCard":359,"__hash__":360},"blog/blog/stories/github-secrets-public.md","An Indie Developer's GitHub Secrets Went Public",{"type":7,"value":8,"toc":318},"minimark",[9,16,21,24,27,31,39,42,52,55,58,62,70,73,86,89,93,96,133,136,140,143,149,152,161,165,170,173,177,180,184,187,191,202,206,210,213,219,223,226,230,233,237,240,249,253,256,259,287,306],[10,11,12],"tldr",{},[13,14,15],"p",{},"An indie SaaS developer pushed to the wrong repository. A private repo's code went to a public one. The commit contained database credentials, API keys, and a JWT secret. GitHub's secret scanning caught most of it, but the 15 minutes before he noticed felt like hours. Everything had to be rotated, and he learned to never have both public and private remotes configured.",[17,18,20],"h2",{"id":19},"the-setup","The Setup",[13,22,23],{},"The developer maintained both open source projects and his own SaaS product. For convenience, he had multiple git remotes configured on his machine. One project had both a private backup remote and a public fork.",[13,25,26],{},"You can probably see where this is going.",[17,28,30],{"id":29},"the-mistake","The Mistake",[13,32,33,34,38],{},"He was working late, finishing a feature for a client integration in his SaaS app. The codebase included a ",[35,36,37],"code",{},"config.js"," file with database credentials. Yes, environment variables exist. He was going to fix it \"soon.\"",[13,40,41],{},"He typed what he thought was a push to his private backup:",[43,44,49],"pre",{"className":45,"code":47,"language":48},[46],"language-text","git push origin main\n","text",[35,50,47],{"__ignoreMap":51},"",[13,53,54],{},"But his current directory wasn't what he thought it was. He was in the wrong project folder. And \"origin\" in that folder pointed to a public repository.",[13,56,57],{},"The push completed. He didn't notice for about 10 minutes.",[17,59,61],{"id":60},"the-discovery","The Discovery",[13,63,64,65,69],{},"He got an email from GitHub: \"Secret scanning alert for ",[66,67,68],"span",{},"repository name",".\"",[13,71,72],{},"His heart stopped. He opened the email and saw it had detected:",[74,75,76,80,83],"ul",{},[77,78,79],"li",{},"A database connection string with username and password",[77,81,82],{},"An AWS access key",[77,84,85],{},"A Stripe secret key",[13,87,88],{},"He sprinted to his computer and checked the repository. There it was. His SaaS product's entire codebase, including the config file with credentials, was now public.",[17,90,92],{"id":91},"the-scramble","The Scramble",[13,94,95],{},"The next 30 minutes were a blur of activity:",[97,98,99,106,112,117,122,127],"ol",{},[77,100,101,105],{},[102,103,104],"strong",{},"Made the repo private"," (this doesn't undo the exposure, but stops new access)",[77,107,108,111],{},[102,109,110],{},"Rotated every credential"," in the config file",[77,113,114],{},[102,115,116],{},"Changed the database password",[77,118,119],{},[102,120,121],{},"Revoked and regenerated the AWS keys",[77,123,124],{},[102,125,126],{},"Rotated the Stripe API key",[77,128,129,132],{},[102,130,131],{},"Changed the JWT secret"," (which invalidated all user sessions)",[13,134,135],{},"Then came the uncomfortable part: figuring out if any of his paying customers had been affected.",[17,137,139],{"id":138},"the-aftermath","The Aftermath",[13,141,142],{},"He checked his logs immediately. He reviewed every database query and API call from the window of exposure.",[144,145,146],"story-block",{},[13,147,148],{},"\"I had about 200 paying users at that point. I kept refreshing the access logs, looking for anything out of the ordinary. The thought of someone accessing my users' data because of my mistake was gut-wrenching.\"",[13,150,151],{},"There was no evidence of unauthorized access, but he couldn't be 100% certain. The credentials had been public for about 15 minutes.",[153,154,155],"warning-box",{},[13,156,157,160],{},[102,158,159],{},"15 minutes is plenty of time."," Automated bots scan GitHub constantly. Research shows credentials can be discovered and tested within minutes of being pushed. Even brief exposure is exposure.",[17,162,164],{"id":163},"what-he-did-wrong","What He Did Wrong",[166,167,169],"h3",{"id":168},"_1-credentials-in-code","1. Credentials in Code",[13,171,172],{},"The fundamental problem. Even in a \"private\" project, credentials shouldn't be in code. Environment variables exist for a reason.",[166,174,176],{"id":175},"_2-multiple-remotes-without-safeguards","2. Multiple Remotes Without Safeguards",[13,178,179],{},"Having both public and private remotes configured is risky. It's too easy to push to the wrong one.",[166,181,183],{"id":182},"_3-no-pre-push-hook","3. No Pre-push Hook",[13,185,186],{},"A pre-push hook could have scanned for secrets and blocked the push. He had one set up on his main projects but not on this one.",[166,188,190],{"id":189},"_4-not-double-checking-the-directory","4. Not Double-Checking the Directory",[13,192,193,194,197,198,201],{},"He assumed he was in the right folder. A quick ",[35,195,196],{},"pwd"," or ",[35,199,200],{},"git remote -v"," would have shown the mistake.",[17,203,205],{"id":204},"changes-he-made","Changes He Made",[166,207,209],{"id":208},"global-git-hooks","Global Git Hooks",[13,211,212],{},"He set up global pre-commit and pre-push hooks that scan for common secret patterns. They run on every repository, not just the ones he remembers to configure.",[43,214,217],{"className":215,"code":216,"language":48},[46],"# ~/.config/git/hooks/pre-push\n# Scans for common secret patterns before any push\nif git diff --cached | grep -qE \"(sk_live|AKIA|password\\s*=)\"; then\n  echo \"Potential secret detected. Push blocked.\"\n  exit 1\nfi\n",[35,218,216],{"__ignoreMap":51},[166,220,222],{"id":221},"removed-public-remotes-from-private-projects","Removed Public Remotes from Private Projects",[13,224,225],{},"If a project is private, it has no public remotes. Period. If he needs to create a public version, he does it in a separate, clean repository.",[166,227,229],{"id":228},"prompt-shows-current-directory","Prompt Shows Current Directory",[13,231,232],{},"He modified his shell prompt to prominently display the current directory and git remote. He can see at a glance where he is and where he's pushing to.",[166,234,236],{"id":235},"separate-workspaces-for-client-and-personal-projects","Separate Workspaces for Client and Personal Projects",[13,238,239],{},"Client work and his SaaS product now live in separate workspace directories. No more mixing open source forks with production codebases on the same machine.",[241,242,243],"lesson-box",{},[13,244,245,248],{},[102,246,247],{},"The lesson:"," Multiple layers of defense matter. Any single safeguard can fail. When you have credentials in code AND multiple remotes AND no pre-push hooks AND you're tired, disaster happens. Remove as many failure points as possible.",[17,250,252],{"id":251},"what-github-did-right","What GitHub Did Right",[13,254,255],{},"GitHub's secret scanning detected the exposed credentials within minutes and sent alerts. For some services (Stripe, AWS), they also notified the providers, who revoked the credentials automatically.",[13,257,258],{},"This is a safety net, not a solution. But it limited the damage significantly.",[260,261,262,269,275,281],"faq-section",{},[263,264,266],"faq-item",{"question":265},"Can I remove secrets from git history?",[13,267,268],{},"Yes, using tools like BFG Repo-Cleaner or git filter-branch. But this doesn't help if the secret was already copied by bots or attackers. Always assume an exposed secret is compromised and rotate it.",[263,270,272],{"question":271},"Does making a repo private remove the exposure?",[13,273,274],{},"No. Making a repo private stops future access, but anyone who already cloned the repo or copied the secrets still has them. Additionally, search engines and archive services may have cached the content.",[263,276,278],{"question":277},"How do I prevent pushing to the wrong remote?",[13,279,280],{},"Remove unnecessary remotes from your git config. Use unique, descriptive remote names. Add confirmation prompts with pre-push hooks. Display current remote in your shell prompt.",[263,282,284],{"question":283},"What if I can't rotate a credential immediately?",[13,285,286],{},"Contact the service provider. Many have emergency processes for compromised credentials. At minimum, add additional authentication (IP restrictions, MFA) while you work on rotation.",[288,289,290,296,301],"related-articles",{},[291,292],"related-card",{"description":293,"href":294,"title":295},"Payment credentials exposed","/blog/stories/api-key-leaked-stripe","When My Stripe API Key Got Leaked",[291,297],{"description":298,"href":299,"title":300},"Secrets in search results","/blog/stories/env-file-indexed","When Google Indexed Our .env File",[291,302],{"description":303,"href":304,"title":305},"Best practices for secrets management","/blog/how-to/secure-api-keys","How to Secure API Keys",[307,308,311,315],"cta-box",{"href":309,"label":310},"/","Start Free Scan",[17,312,314],{"id":313},"scan-for-exposed-secrets","Scan for Exposed Secrets",[13,316,317],{},"Check your repositories for accidentally committed credentials.",{"title":51,"searchDepth":319,"depth":319,"links":320},2,[321,322,323,324,325,326,333,339,340],{"id":19,"depth":319,"text":20},{"id":29,"depth":319,"text":30},{"id":60,"depth":319,"text":61},{"id":91,"depth":319,"text":92},{"id":138,"depth":319,"text":139},{"id":163,"depth":319,"text":164,"children":327},[328,330,331,332],{"id":168,"depth":329,"text":169},3,{"id":175,"depth":329,"text":176},{"id":182,"depth":329,"text":183},{"id":189,"depth":329,"text":190},{"id":204,"depth":319,"text":205,"children":334},[335,336,337,338],{"id":208,"depth":329,"text":209},{"id":221,"depth":329,"text":222},{"id":228,"depth":329,"text":229},{"id":235,"depth":329,"text":236},{"id":251,"depth":319,"text":252},{"id":313,"depth":319,"text":314},"stories","2026-02-05","The story of an indie SaaS developer accidentally pushing secrets to a public GitHub repository. How it happened, how fast they were found, and the scramble to fix everything.",false,"md",null,{},true,"What happens when an indie developer accidentally pushes secrets to a public repository.","/blog/stories/github-secrets-public","7 min read","[object Object]","BlogPosting",{"title":5,"description":343},{"loc":350},"blog/stories/github-secrets-public",[358],"Git Security","summary_large_image","rPSmvQoVCx-7NPULy-H_-yNchlqI_RYBx-jAyOFwNJU",1775843936498]