[{"data":1,"prerenderedAt":2025},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely":1992,"ancestors:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely":2000,"children:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely":2005,"related:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely":2006},{"id":4,"title":5,"body":6,"dateModified":1958,"datePublished":1958,"description":1959,"extension":1960,"faq":1961,"keyword":1970,"kind":1971,"meta":1972,"navigation":306,"path":1981,"seo":1982,"slug":1985,"stem":1986,"topics":1987,"type":1990,"__hash__":1991},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely\u002Findex.md","Store Credentials for Excel Report Scripts Safely",{"type":7,"value":8,"toc":1946},"minimark",[9,19,24,70,73,278,282,285,529,536,540,551,582,654,712,733,737,744,953,967,971,974,1199,1211,1214,1427,1431,1434,1736,1814,1826,1830,1871,1875,1881,1885,1891,1897,1903,1909,1913,1942],[10,11,12,13,18],"p",{},"Report scripts need credentials: the database the numbers come from, the mail server that sends the result, the API of the CRM or the file share. The quickest way to make a script work is to type the password into it, and that is how passwords end up in version control, in emailed copies of the script, in screenshots and in the logs of every failed run. The fix is not complicated, but it depends on where the script runs. This page, part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",", walks through the options from a laptop to a cloud job, with code that runs as shown, and ends with the piece most often forgotten: keeping secrets out of logs.",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install python-dotenv keyring boto3\npip install \"moto[secretsmanager]\"      # only for the local test of the cloud example\n","bash","",[32,33,34,56],"code",{"__ignoreMap":30},[35,36,39,43,47,50,53],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," python-dotenv",[35,51,52],{"class":45}," keyring",[35,54,55],{"class":45}," boto3\n",[35,57,59,61,63,66],{"class":37,"line":58},2,[35,60,42],{"class":41},[35,62,46],{"class":45},[35,64,65],{"class":45}," \"moto[secretsmanager]\"",[35,67,69],{"class":68},"s-wDw","      # only for the local test of the cloud example\n",[10,71,72],{},"The examples read a database password and an SMTP password. None of them is real; the cloud example runs against moto's in-memory imitation of AWS Secrets Manager.",[74,75,83,84,83,88,83,92,83,100,83,110,83,121,83,127,83,131,83,135,83,138,83,143,83,152,83,157,83,164,83,171,83,176,83,181,83,184,83,187,83,191,83,194,83,197,83,203,83,207,83,211,83,215,83,218,83,221,83,225,83,228,83,233,83,240,83,244,83,247,83,249,83,252,83,256,83,259,83,264,83,271,83,275],"svg",{"viewBox":76,"role":77,"ariaLabelledBy":78,"xmlns":81,"style":82},"4 0 752 290","img",[79,80],"cr-where-t","cr-where-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[85,86,87],"title",{"id":79},"Where should the secret come from?",[89,90,91],"desc",{"id":80},"A decision diagram. On a developer laptop use environment variables loaded from a git-ignored .env file; for a scheduled job on a desktop use the operating system keyring; on a server use environment variables set by the service manager or a secret manager; in the cloud prefer a managed identity that needs no secret, and a secret manager otherwise.",[93,94],"rect",{"x":95,"y":96,"width":97,"height":98,"fill":99},"4","0","752","290","#ffffff",[93,101],{"x":102,"y":103,"width":104,"height":105,"rx":106,"fill":107,"stroke":108,"style":109},"240.0","16.0","280.0","75.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[111,112,114],"g",{"transform":113},"translate(368.0,29.0) scale(1.000)",[115,116],"path",{"d":117,"fill":118,"stroke":119,"style":120},"M8 11a4 4 0 1 0 .01 0z M11 13l9-9 M17 7l2 2 M15 9l2 2","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[122,123,126],"text",{"x":124,"y":105,"style":125},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Where does the report run?",[37,128],{"x1":124,"y1":129,"x2":124,"y2":130,"stroke":108,"style":109},"91.5","111.5",[37,132],{"x1":133,"y1":130,"x2":134,"y2":130,"stroke":108,"style":109},"104.0","656.0",[37,136],{"x1":133,"y1":130,"x2":133,"y2":137,"stroke":108,"style":109},"157.5",[139,140],"polygon",{"points":141,"fill":142},"104.0,161.5 99.0,152.5 109.0,152.5","#5b5cf0",[93,144],{"x":145,"y":146,"width":147,"height":148,"rx":149,"fill":99,"stroke":150,"style":151},"45.1","121.5","117.8","21.0","10","var(--line,#cdd5e6)","stroke-width:1.2px",[122,153,156],{"x":133,"y":154,"style":155},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","developer laptop",[93,158],{"x":159,"y":160,"width":161,"height":162,"rx":106,"fill":163,"stroke":150,"style":109},"20.0","163.5","168.0","110.5","#e7ebef",[111,165,167],{"transform":166},"translate(92.0,185.2) scale(1.000)",[115,168],{"d":169,"fill":118,"stroke":170,"style":120},"M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M4 21c0-4 3.6-7 8-7s8 3 8 7","var(--text,#172033)",[122,172,175],{"x":133,"y":173,"style":174},"231.8","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",".env → environment",[122,177,180],{"x":133,"y":178,"style":179},"251.2","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","git-ignored file",[37,182],{"x1":183,"y1":130,"x2":183,"y2":137,"stroke":108,"style":109},"288.0",[139,185],{"points":186,"fill":142},"288.0,161.5 283.0,152.5 293.0,152.5",[93,188],{"x":189,"y":146,"width":190,"height":148,"rx":149,"fill":99,"stroke":150,"style":151},"244.4","87.1",[122,192,193],{"x":183,"y":154,"style":155},"desktop job",[93,195],{"x":196,"y":160,"width":161,"height":162,"rx":106,"fill":107,"stroke":108,"style":109},"204.0",[111,198,200],{"transform":199},"translate(276.0,176.5) scale(1.000)",[115,201],{"d":202,"fill":118,"stroke":119,"style":120},"M6 11h12v9H6z M8.5 11V8a3.5 3.5 0 0 1 7 0v3",[122,204,206],{"x":183,"y":205,"style":125},"223.0","OS keyring",[122,208,210],{"x":183,"y":209,"style":179},"242.5","Credential Manager,",[122,212,214],{"x":183,"y":213,"style":179},"260.0","Keychain",[37,216],{"x1":217,"y1":130,"x2":217,"y2":137,"stroke":108,"style":109},"472.0",[139,219],{"points":220,"fill":142},"472.0,161.5 467.0,152.5 477.0,152.5",[93,222],{"x":223,"y":146,"width":224,"height":148,"rx":149,"fill":99,"stroke":150,"style":151},"443.9","56.2",[122,226,227],{"x":217,"y":154,"style":155},"server",[93,229],{"x":230,"y":160,"width":161,"height":162,"rx":106,"fill":231,"stroke":232,"style":109},"388.0","#d9f4f1","var(--teal,#0f9488)",[111,234,236],{"transform":235},"translate(460.0,185.2) scale(1.000)",[115,237],{"d":238,"fill":118,"stroke":239,"style":120},"M4 4h16v6H4z M4 14h16v6H4z M8 7h.01 M8 17h.01","var(--teal-ink,#0b6157)",[122,241,243],{"x":217,"y":173,"style":242},"font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Service environment",[122,245,246],{"x":217,"y":178,"style":179},"or a secret manager",[37,248],{"x1":134,"y1":130,"x2":134,"y2":137,"stroke":108,"style":109},[139,250],{"points":251,"fill":142},"656.0,161.5 651.0,152.5 661.0,152.5",[93,253],{"x":254,"y":146,"width":255,"height":148,"rx":149,"fill":99,"stroke":150,"style":151},"630.5","50.9",[122,257,258],{"x":134,"y":154,"style":155},"cloud",[93,260],{"x":261,"y":160,"width":161,"height":162,"rx":106,"fill":262,"stroke":263,"style":109},"572.0","#fdefd8","var(--gold,#b4740a)",[111,265,267],{"transform":266},"translate(644.0,185.2) scale(1.000)",[115,268],{"d":269,"fill":118,"stroke":270,"style":120},"M7 18h10a4 4 0 0 0 .6-8A6 6 0 0 0 6.1 11.4 3.4 3.4 0 0 0 7 18z","var(--gold-ink,#7a4e06)",[122,272,274],{"x":134,"y":173,"style":273},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Managed identity",[122,276,277],{"x":134,"y":178,"style":179},"no secret at all",[20,279,281],{"id":280},"step-1-read-secrets-from-the-environment-and-fail-clearly","Step 1 — Read secrets from the environment, and fail clearly",[10,283,284],{},"The environment is the lowest common denominator: every scheduler, container platform and CI system can set environment variables, and code that reads them works everywhere. Read them in one place and fail with a message that names every missing variable at once:",[25,286,290],{"className":287,"code":288,"language":289,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","import os\n\nREQUIRED = [\"REPORT_DB_URL\", \"REPORT_DB_PASSWORD\", \"SMTP_PASSWORD\"]\n\nclass ConfigError(RuntimeError):\n    pass\n\ndef load_secrets(names=REQUIRED):\n    missing = [n for n in names if not os.environ.get(n)]\n    if missing:\n        raise ConfigError(f\"missing environment variables: {', '.join(missing)}\")\n    return {n: os.environ[n] for n in names}\n\ntry:\n    load_secrets()\nexcept ConfigError as err:\n    print(err)\n","python",[32,291,292,302,308,338,343,361,367,372,392,424,433,467,485,490,499,505,520],{"__ignoreMap":30},[35,293,294,298],{"class":37,"line":38},[35,295,297],{"class":296},"s-kum","import",[35,299,301],{"class":300},"skGVy"," os\n",[35,303,304],{"class":37,"line":58},[35,305,307],{"emptyLinePlaceholder":306},true,"\n",[35,309,311,315,318,321,324,327,330,332,335],{"class":37,"line":310},3,[35,312,314],{"class":313},"sP0c6","REQUIRED",[35,316,317],{"class":296}," =",[35,319,320],{"class":300}," [",[35,322,323],{"class":45},"\"REPORT_DB_URL\"",[35,325,326],{"class":300},", ",[35,328,329],{"class":45},"\"REPORT_DB_PASSWORD\"",[35,331,326],{"class":300},[35,333,334],{"class":45},"\"SMTP_PASSWORD\"",[35,336,337],{"class":300},"]\n",[35,339,341],{"class":37,"line":340},4,[35,342,307],{"emptyLinePlaceholder":306},[35,344,346,349,352,355,358],{"class":37,"line":345},5,[35,347,348],{"class":296},"class",[35,350,351],{"class":41}," ConfigError",[35,353,354],{"class":300},"(",[35,356,357],{"class":313},"RuntimeError",[35,359,360],{"class":300},"):\n",[35,362,364],{"class":37,"line":363},6,[35,365,366],{"class":296},"    pass\n",[35,368,370],{"class":37,"line":369},7,[35,371,307],{"emptyLinePlaceholder":306},[35,373,375,378,382,385,388,390],{"class":37,"line":374},8,[35,376,377],{"class":296},"def",[35,379,381],{"class":380},"s_Opv"," load_secrets",[35,383,384],{"class":300},"(names",[35,386,387],{"class":296},"=",[35,389,314],{"class":313},[35,391,360],{"class":300},[35,393,395,398,400,403,406,409,412,415,418,421],{"class":37,"line":394},9,[35,396,397],{"class":300},"    missing ",[35,399,387],{"class":296},[35,401,402],{"class":300}," [n ",[35,404,405],{"class":296},"for",[35,407,408],{"class":300}," n ",[35,410,411],{"class":296},"in",[35,413,414],{"class":300}," names ",[35,416,417],{"class":296},"if",[35,419,420],{"class":296}," not",[35,422,423],{"class":300}," os.environ.get(n)]\n",[35,425,427,430],{"class":37,"line":426},10,[35,428,429],{"class":296},"    if",[35,431,432],{"class":300}," missing:\n",[35,434,436,439,442,445,448,452,455,458,461,464],{"class":37,"line":435},11,[35,437,438],{"class":296},"        raise",[35,440,441],{"class":300}," ConfigError(",[35,443,444],{"class":296},"f",[35,446,447],{"class":45},"\"missing environment variables: ",[35,449,451],{"class":450},"sSjpA","{",[35,453,454],{"class":45},"', '",[35,456,457],{"class":300},".join(missing)",[35,459,460],{"class":450},"}",[35,462,463],{"class":45},"\"",[35,465,466],{"class":300},")\n",[35,468,470,473,476,478,480,482],{"class":37,"line":469},12,[35,471,472],{"class":296},"    return",[35,474,475],{"class":300}," {n: os.environ[n] ",[35,477,405],{"class":296},[35,479,408],{"class":300},[35,481,411],{"class":296},[35,483,484],{"class":300}," names}\n",[35,486,488],{"class":37,"line":487},13,[35,489,307],{"emptyLinePlaceholder":306},[35,491,493,496],{"class":37,"line":492},14,[35,494,495],{"class":296},"try",[35,497,498],{"class":300},":\n",[35,500,502],{"class":37,"line":501},15,[35,503,504],{"class":300},"    load_secrets()\n",[35,506,508,511,514,517],{"class":37,"line":507},16,[35,509,510],{"class":296},"except",[35,512,513],{"class":300}," ConfigError ",[35,515,516],{"class":296},"as",[35,518,519],{"class":300}," err:\n",[35,521,523,526],{"class":37,"line":522},17,[35,524,525],{"class":313},"    print",[35,527,528],{"class":300},"(err)\n",[10,530,531,532,535],{},"Checking every variable up front, before the script has spent ten minutes querying and formatting, turns a confusing ",[32,533,534],{},"None"," failure deep in the SMTP code into one line naming exactly what to set. It also makes the script’s needs explicit: the list at the top is the documentation of what it expects, and a new colleague setting the job up on another machine sees at once what to provide.",[20,537,539],{"id":538},"step-2-a-env-file-for-development-only","Step 2 — A .env file for development only",[10,541,542,543,546,547,550],{},"Typing ",[32,544,545],{},"export"," lines before every test run is tedious, so developers keep local values in a ",[32,548,549],{},".env"," file that python-dotenv loads into the environment. The file must never be committed:",[25,552,556],{"className":553,"code":554,"language":555,"meta":30,"style":30},"language-ini shiki shiki-themes github-light github-dark-high-contrast","REPORT_DB_URL=postgresql:\u002F\u002Freports@db.internal:5432\u002Fsales\nREPORT_DB_PASSWORD=dev-only-password\nSMTP_PASSWORD=dev-smtp-password\n","ini",[32,557,558,566,574],{"__ignoreMap":30},[35,559,560,563],{"class":37,"line":38},[35,561,562],{"class":296},"REPORT_DB_URL",[35,564,565],{"class":300},"=postgresql:\u002F\u002Freports@db.internal:5432\u002Fsales\n",[35,567,568,571],{"class":37,"line":58},[35,569,570],{"class":296},"REPORT_DB_PASSWORD",[35,572,573],{"class":300},"=dev-only-password\n",[35,575,576,579],{"class":37,"line":310},[35,577,578],{"class":296},"SMTP_PASSWORD",[35,580,581],{"class":300},"=dev-smtp-password\n",[25,583,585],{"className":287,"code":584,"language":289,"meta":30,"style":30},"from dotenv import load_dotenv\n\nloaded = load_dotenv(\".env\", override=False)\nsecrets = load_secrets()\nprint(loaded, sorted(secrets))\n",[32,586,587,600,604,630,640],{"__ignoreMap":30},[35,588,589,592,595,597],{"class":37,"line":38},[35,590,591],{"class":296},"from",[35,593,594],{"class":300}," dotenv ",[35,596,297],{"class":296},[35,598,599],{"class":300}," load_dotenv\n",[35,601,602],{"class":37,"line":58},[35,603,307],{"emptyLinePlaceholder":306},[35,605,606,609,611,614,617,619,623,625,628],{"class":37,"line":310},[35,607,608],{"class":300},"loaded ",[35,610,387],{"class":296},[35,612,613],{"class":300}," load_dotenv(",[35,615,616],{"class":45},"\".env\"",[35,618,326],{"class":300},[35,620,622],{"class":621},"sa561","override",[35,624,387],{"class":296},[35,626,627],{"class":313},"False",[35,629,466],{"class":300},[35,631,632,635,637],{"class":37,"line":340},[35,633,634],{"class":300},"secrets ",[35,636,387],{"class":296},[35,638,639],{"class":300}," load_secrets()\n",[35,641,642,645,648,651],{"class":37,"line":345},[35,643,644],{"class":313},"print",[35,646,647],{"class":300},"(loaded, ",[35,649,650],{"class":313},"sorted",[35,652,653],{"class":300},"(secrets))\n",[25,655,657],{"className":27,"code":656,"language":29,"meta":30,"style":30},"printf '.env\\n*.env\\n' > .gitignore && git init -q . && git check-ignore -v .env && rm -rf .git\n",[32,658,659],{"__ignoreMap":30},[35,660,661,664,667,670,673,676,679,682,685,688,690,692,695,698,701,703,706,709],{"class":37,"line":38},[35,662,663],{"class":313},"printf",[35,665,666],{"class":45}," '.env\\n*.env\\n'",[35,668,669],{"class":296}," >",[35,671,672],{"class":45}," .gitignore",[35,674,675],{"class":300}," && ",[35,677,678],{"class":41},"git",[35,680,681],{"class":45}," init",[35,683,684],{"class":313}," -q",[35,686,687],{"class":45}," .",[35,689,675],{"class":300},[35,691,678],{"class":41},[35,693,694],{"class":45}," check-ignore",[35,696,697],{"class":313}," -v",[35,699,700],{"class":45}," .env",[35,702,675],{"class":300},[35,704,705],{"class":41},"rm",[35,707,708],{"class":313}," -rf",[35,710,711],{"class":45}," .git\n",[10,713,714,717,718,720,721,724,725,729,730,732],{},[32,715,716],{},"override=False"," means real environment variables win over the file, so the same code uses the server's values in production and the file's values on a laptop. The git check confirms ",[32,719,549],{}," is ignored before anything is ever committed; keep a ",[32,722,723],{},".env.example"," with the variable ",[726,727,728],"em",{},"names"," and dummy values in the repository instead, so new developers know what to set. A ",[32,731,549],{}," file on a production server is just a password in a file with an unusual name — use the next options there.",[20,734,736],{"id":735},"step-3-the-operating-system-keyring-on-desktops","Step 3 — The operating system keyring on desktops",[10,738,739,740,743],{},"For a report scheduled on a Windows or Mac desktop, the operating system already has a secure store: Windows Credential Manager and the macOS Keychain, encrypted and tied to the user account. The ",[32,741,742],{},"keyring"," library reads and writes them with one API:",[25,745,747],{"className":287,"code":746,"language":289,"meta":30,"style":30},"import keyring\nfrom keyring.backend import KeyringBackend\n\nclass MemoryKeyring(KeyringBackend):\n    priority = 1\n    def __init__(self):\n        self.store = {}\n    def set_password(self, service, username, password):\n        self.store[(service, username)] = password\n    def get_password(self, service, username):\n        return self.store.get((service, username))\n    def delete_password(self, service, username):\n        self.store.pop((service, username), None)\n\nkeyring.set_keyring(MemoryKeyring())          # stand-in for Credential Manager \u002F Keychain in this test\n\nkeyring.set_password(\"weekly-sales-report\", \"reports@acme.example\", \"s3cret-smtp\")\nprint(keyring.get_password(\"weekly-sales-report\", \"reports@acme.example\"))\nprint(keyring.get_password(\"weekly-sales-report\", \"nobody\"))\n",[32,748,749,756,768,772,787,797,808,821,831,843,853,864,873,884,888,896,900,920,937],{"__ignoreMap":30},[35,750,751,753],{"class":37,"line":38},[35,752,297],{"class":296},[35,754,755],{"class":300}," keyring\n",[35,757,758,760,763,765],{"class":37,"line":58},[35,759,591],{"class":296},[35,761,762],{"class":300}," keyring.backend ",[35,764,297],{"class":296},[35,766,767],{"class":300}," KeyringBackend\n",[35,769,770],{"class":37,"line":310},[35,771,307],{"emptyLinePlaceholder":306},[35,773,774,776,779,781,785],{"class":37,"line":340},[35,775,348],{"class":296},[35,777,778],{"class":41}," MemoryKeyring",[35,780,354],{"class":300},[35,782,784],{"class":783},"sHg5J","KeyringBackend",[35,786,360],{"class":300},[35,788,789,792,794],{"class":37,"line":345},[35,790,791],{"class":300},"    priority ",[35,793,387],{"class":296},[35,795,796],{"class":313}," 1\n",[35,798,799,802,805],{"class":37,"line":363},[35,800,801],{"class":296},"    def",[35,803,804],{"class":313}," __init__",[35,806,807],{"class":300},"(self):\n",[35,809,810,813,816,818],{"class":37,"line":369},[35,811,812],{"class":313},"        self",[35,814,815],{"class":300},".store ",[35,817,387],{"class":296},[35,819,820],{"class":300}," {}\n",[35,822,823,825,828],{"class":37,"line":374},[35,824,801],{"class":296},[35,826,827],{"class":380}," set_password",[35,829,830],{"class":300},"(self, service, username, password):\n",[35,832,833,835,838,840],{"class":37,"line":394},[35,834,812],{"class":313},[35,836,837],{"class":300},".store[(service, username)] ",[35,839,387],{"class":296},[35,841,842],{"class":300}," password\n",[35,844,845,847,850],{"class":37,"line":426},[35,846,801],{"class":296},[35,848,849],{"class":380}," get_password",[35,851,852],{"class":300},"(self, service, username):\n",[35,854,855,858,861],{"class":37,"line":435},[35,856,857],{"class":296},"        return",[35,859,860],{"class":313}," self",[35,862,863],{"class":300},".store.get((service, username))\n",[35,865,866,868,871],{"class":37,"line":469},[35,867,801],{"class":296},[35,869,870],{"class":380}," delete_password",[35,872,852],{"class":300},[35,874,875,877,880,882],{"class":37,"line":487},[35,876,812],{"class":313},[35,878,879],{"class":300},".store.pop((service, username), ",[35,881,534],{"class":313},[35,883,466],{"class":300},[35,885,886],{"class":37,"line":492},[35,887,307],{"emptyLinePlaceholder":306},[35,889,890,893],{"class":37,"line":501},[35,891,892],{"class":300},"keyring.set_keyring(MemoryKeyring())          ",[35,894,895],{"class":68},"# stand-in for Credential Manager \u002F Keychain in this test\n",[35,897,898],{"class":37,"line":507},[35,899,307],{"emptyLinePlaceholder":306},[35,901,902,905,908,910,913,915,918],{"class":37,"line":522},[35,903,904],{"class":300},"keyring.set_password(",[35,906,907],{"class":45},"\"weekly-sales-report\"",[35,909,326],{"class":300},[35,911,912],{"class":45},"\"reports@acme.example\"",[35,914,326],{"class":300},[35,916,917],{"class":45},"\"s3cret-smtp\"",[35,919,466],{"class":300},[35,921,923,925,928,930,932,934],{"class":37,"line":922},18,[35,924,644],{"class":313},[35,926,927],{"class":300},"(keyring.get_password(",[35,929,907],{"class":45},[35,931,326],{"class":300},[35,933,912],{"class":45},[35,935,936],{"class":300},"))\n",[35,938,940,942,944,946,948,951],{"class":37,"line":939},19,[35,941,644],{"class":313},[35,943,927],{"class":300},[35,945,907],{"class":45},[35,947,326],{"class":300},[35,949,950],{"class":45},"\"nobody\"",[35,952,936],{"class":300},[10,954,955,956,959,960,962,963,966],{},"On a real desktop the ",[32,957,958],{},"set_keyring"," line is omitted: ",[32,961,742],{}," picks the platform's store automatically, and the password is set once — interactively, ",[32,964,965],{},"keyring set weekly-sales-report reports@acme.example"," — then read by the script at every run. The secret is protected by the user's login; a scheduled task must run as that same user to read it. On headless Linux servers there is usually no keyring service, which is why servers use the next options.",[20,968,970],{"id":969},"step-4-a-secret-manager-on-servers-and-in-the-cloud","Step 4 — A secret manager on servers and in the cloud",[10,972,973],{},"A secret manager — AWS Secrets Manager, Azure Key Vault, Google Secret Manager, HashiCorp Vault — stores secrets centrally, controls who may read each one, logs every access and supports rotation. The script fetches the secret at start-up with credentials it already has from its environment:",[25,975,977],{"className":287,"code":976,"language":289,"meta":30,"style":30},"import json\nimport boto3\nfrom moto import mock_aws\n\nos.environ.update(AWS_ACCESS_KEY_ID=\"testing\", AWS_SECRET_ACCESS_KEY=\"testing\", AWS_DEFAULT_REGION=\"eu-west-2\")\n\ndef fetch_secret(client, name):\n    value = client.get_secret_value(SecretId=name)[\"SecretString\"]\n    return json.loads(value)\n\nwith mock_aws():\n    sm = boto3.client(\"secretsmanager\")\n    sm.create_secret(Name=\"reports\u002Fsales-db\", SecretString=json.dumps({\"username\": \"reports\", \"password\": \"rotated-2026-09\"}))\n    db = fetch_secret(sm, \"reports\u002Fsales-db\")\n    print(sorted(db), \"password length:\", len(db[\"password\"]))\n",[32,978,979,986,992,1004,1008,1042,1046,1056,1079,1086,1090,1098,1113,1158,1172],{"__ignoreMap":30},[35,980,981,983],{"class":37,"line":38},[35,982,297],{"class":296},[35,984,985],{"class":300}," json\n",[35,987,988,990],{"class":37,"line":58},[35,989,297],{"class":296},[35,991,55],{"class":300},[35,993,994,996,999,1001],{"class":37,"line":310},[35,995,591],{"class":296},[35,997,998],{"class":300}," moto ",[35,1000,297],{"class":296},[35,1002,1003],{"class":300}," mock_aws\n",[35,1005,1006],{"class":37,"line":340},[35,1007,307],{"emptyLinePlaceholder":306},[35,1009,1010,1013,1016,1018,1021,1023,1026,1028,1030,1032,1035,1037,1040],{"class":37,"line":345},[35,1011,1012],{"class":300},"os.environ.update(",[35,1014,1015],{"class":621},"AWS_ACCESS_KEY_ID",[35,1017,387],{"class":296},[35,1019,1020],{"class":45},"\"testing\"",[35,1022,326],{"class":300},[35,1024,1025],{"class":621},"AWS_SECRET_ACCESS_KEY",[35,1027,387],{"class":296},[35,1029,1020],{"class":45},[35,1031,326],{"class":300},[35,1033,1034],{"class":621},"AWS_DEFAULT_REGION",[35,1036,387],{"class":296},[35,1038,1039],{"class":45},"\"eu-west-2\"",[35,1041,466],{"class":300},[35,1043,1044],{"class":37,"line":363},[35,1045,307],{"emptyLinePlaceholder":306},[35,1047,1048,1050,1053],{"class":37,"line":369},[35,1049,377],{"class":296},[35,1051,1052],{"class":380}," fetch_secret",[35,1054,1055],{"class":300},"(client, name):\n",[35,1057,1058,1061,1063,1066,1069,1071,1074,1077],{"class":37,"line":374},[35,1059,1060],{"class":300},"    value ",[35,1062,387],{"class":296},[35,1064,1065],{"class":300}," client.get_secret_value(",[35,1067,1068],{"class":621},"SecretId",[35,1070,387],{"class":296},[35,1072,1073],{"class":300},"name)[",[35,1075,1076],{"class":45},"\"SecretString\"",[35,1078,337],{"class":300},[35,1080,1081,1083],{"class":37,"line":394},[35,1082,472],{"class":296},[35,1084,1085],{"class":300}," json.loads(value)\n",[35,1087,1088],{"class":37,"line":426},[35,1089,307],{"emptyLinePlaceholder":306},[35,1091,1092,1095],{"class":37,"line":435},[35,1093,1094],{"class":296},"with",[35,1096,1097],{"class":300}," mock_aws():\n",[35,1099,1100,1103,1105,1108,1111],{"class":37,"line":469},[35,1101,1102],{"class":300},"    sm ",[35,1104,387],{"class":296},[35,1106,1107],{"class":300}," boto3.client(",[35,1109,1110],{"class":45},"\"secretsmanager\"",[35,1112,466],{"class":300},[35,1114,1115,1118,1121,1123,1126,1128,1131,1133,1136,1139,1142,1145,1147,1150,1152,1155],{"class":37,"line":487},[35,1116,1117],{"class":300},"    sm.create_secret(",[35,1119,1120],{"class":621},"Name",[35,1122,387],{"class":296},[35,1124,1125],{"class":45},"\"reports\u002Fsales-db\"",[35,1127,326],{"class":300},[35,1129,1130],{"class":621},"SecretString",[35,1132,387],{"class":296},[35,1134,1135],{"class":300},"json.dumps({",[35,1137,1138],{"class":45},"\"username\"",[35,1140,1141],{"class":300},": ",[35,1143,1144],{"class":45},"\"reports\"",[35,1146,326],{"class":300},[35,1148,1149],{"class":45},"\"password\"",[35,1151,1141],{"class":300},[35,1153,1154],{"class":45},"\"rotated-2026-09\"",[35,1156,1157],{"class":300},"}))\n",[35,1159,1160,1163,1165,1168,1170],{"class":37,"line":492},[35,1161,1162],{"class":300},"    db ",[35,1164,387],{"class":296},[35,1166,1167],{"class":300}," fetch_secret(sm, ",[35,1169,1125],{"class":45},[35,1171,466],{"class":300},[35,1173,1174,1176,1178,1180,1183,1186,1188,1191,1194,1196],{"class":37,"line":501},[35,1175,525],{"class":313},[35,1177,354],{"class":300},[35,1179,650],{"class":313},[35,1181,1182],{"class":300},"(db), ",[35,1184,1185],{"class":45},"\"password length:\"",[35,1187,326],{"class":300},[35,1189,1190],{"class":313},"len",[35,1192,1193],{"class":300},"(db[",[35,1195,1149],{"class":45},[35,1197,1198],{"class":300},"]))\n",[10,1200,1201,1202,1205,1206,1210],{},"The secret is JSON holding both the username and the password, so rotating both is one update. Fetch it once per run and keep it in memory, never in a file or a cache on disk. On AWS, the job's IAM role grants ",[32,1203,1204],{},"secretsmanager:GetSecretValue"," on that one secret; no access key is stored anywhere. ",[14,1207,1209],{"href":1208},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-an-excel-report-with-amazon-ses\u002F","Send an Excel Report with Amazon SES"," relies on the same role-based credentials.",[10,1212,1213],{},"Better still is a secret you do not have: Azure managed identities, Google service accounts attached to Cloud Run, AWS IAM roles and PostgreSQL IAM authentication let the job authenticate as itself, with short-lived tokens issued by the platform. When the database or API supports it, there is nothing to store and nothing to rotate.",[74,1215,83,1220,83,1223,83,1226,83,1229,83,1234,83,1238,83,1244,83,1247,83,1251,83,1254,83,1258,83,1262,83,1268,83,1270,83,1277,83,1279,83,1287,83,1289,83,1294,83,1297,83,1301,83,1303,83,1308,83,1310,83,1314,83,1316,83,1321,83,1324,83,1328,83,1330,83,1334,83,1336,83,1341,83,1343,83,1348,83,1351,83,1354,83,1356,83,1361,83,1363,83,1368,83,1370,83,1375,83,1378,83,1382,83,1384,83,1389,83,1391,83,1396,83,1398,83,1403,83,1406,83,1409,83,1411,83,1416,83,1418,83,1422,83,1424],{"viewBox":1216,"role":77,"ariaLabelledBy":1217,"xmlns":81,"style":82},"4 0 752 276",[1218,1219],"cr-options-t","cr-options-d",[85,1221,1222],{"id":1218},"The options compared",[89,1224,1225],{"id":1219},"A comparison of where secrets can live. A hard-coded value is visible in code and history. An environment variable works everywhere but is visible to the process's environment. A .env file suits development only. The OS keyring is encrypted per user. A secret manager adds access control, audit and rotation. A managed identity has no secret to leak.",[93,1227],{"x":95,"y":96,"width":97,"height":1228,"fill":99},"276",[93,1230],{"x":159,"y":103,"width":1231,"height":1232,"rx":96,"fill":99,"stroke":150,"style":1233},"184.2","40.0","stroke-width:1px",[93,1235],{"x":1236,"y":103,"width":1237,"height":1232,"rx":96,"fill":107,"stroke":108,"style":151},"204.2","178.6",[122,1239,1243],{"x":1240,"y":1241,"style":1242},"293.5","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","In the code or repo",[93,1245],{"x":1246,"y":103,"width":1237,"height":1232,"rx":96,"fill":107,"stroke":108,"style":151},"382.8",[122,1248,1250],{"x":1249,"y":1241,"style":1242},"472.1","Encrypted at rest",[93,1252],{"x":1253,"y":103,"width":1237,"height":1232,"rx":96,"fill":107,"stroke":108,"style":151},"561.4",[122,1255,1257],{"x":1256,"y":1241,"style":1242},"650.7","Audit and rotation",[93,1259],{"x":159,"y":1260,"width":1231,"height":1261,"rx":96,"fill":163,"stroke":150,"style":1233},"56.0","34.0",[122,1263,1267],{"x":1264,"y":1265,"style":1266},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","Hard-coded in the script",[93,1269],{"x":1236,"y":1260,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1271,1273],{"transform":1272},"translate(284.5,64.0) scale(0.750)",[115,1274],{"d":1275,"fill":118,"stroke":232,"style":1276},"M5 12.5l4.5 4.5L19 7","stroke-width:2.40px;stroke-linecap:round;stroke-linejoin:round",[93,1278],{"x":1246,"y":1260,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1280,1282],{"transform":1281},"translate(464.1,65.0) scale(0.667)",[115,1283],{"d":1284,"fill":118,"stroke":1285,"style":1286},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[93,1288],{"x":1253,"y":1260,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1290,1292],{"transform":1291},"translate(642.7,65.0) scale(0.667)",[115,1293],{"d":1284,"fill":118,"stroke":1285,"style":1286},[93,1295],{"x":159,"y":1296,"width":1231,"height":1261,"rx":96,"fill":163,"stroke":150,"style":1233},"90.0",[122,1298,1300],{"x":1264,"y":1299,"style":1266},"112.0","Environment variable",[93,1302],{"x":1236,"y":1296,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1304,1306],{"transform":1305},"translate(285.5,99.0) scale(0.667)",[115,1307],{"d":1284,"fill":118,"stroke":1285,"style":1286},[93,1309],{"x":1246,"y":1296,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[122,1311,1313],{"x":1249,"y":1312,"style":179},"111.0","depends",[93,1315],{"x":1253,"y":1296,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1317,1319],{"transform":1318},"translate(642.7,99.0) scale(0.667)",[115,1320],{"d":1284,"fill":118,"stroke":1285,"style":1286},[93,1322],{"x":159,"y":1323,"width":1231,"height":1261,"rx":96,"fill":163,"stroke":150,"style":1233},"124.0",[122,1325,1327],{"x":1264,"y":1326,"style":1266},"146.0",".env file (dev only)",[93,1329],{"x":1236,"y":1323,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[122,1331,1333],{"x":1240,"y":1332,"style":179},"145.0","if not ignored",[93,1335],{"x":1246,"y":1323,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1337,1339],{"transform":1338},"translate(464.1,133.0) scale(0.667)",[115,1340],{"d":1284,"fill":118,"stroke":1285,"style":1286},[93,1342],{"x":1253,"y":1323,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1344,1346],{"transform":1345},"translate(642.7,133.0) scale(0.667)",[115,1347],{"d":1284,"fill":118,"stroke":1285,"style":1286},[93,1349],{"x":159,"y":1350,"width":1231,"height":1261,"rx":96,"fill":163,"stroke":150,"style":1233},"158.0",[122,1352,206],{"x":1264,"y":1353,"style":1266},"180.0",[93,1355],{"x":1236,"y":1350,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1357,1359],{"transform":1358},"translate(285.5,167.0) scale(0.667)",[115,1360],{"d":1284,"fill":118,"stroke":1285,"style":1286},[93,1362],{"x":1246,"y":1350,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1364,1366],{"transform":1365},"translate(463.1,166.0) scale(0.750)",[115,1367],{"d":1275,"fill":118,"stroke":232,"style":1276},[93,1369],{"x":1253,"y":1350,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1371,1373],{"transform":1372},"translate(642.7,167.0) scale(0.667)",[115,1374],{"d":1284,"fill":118,"stroke":1285,"style":1286},[93,1376],{"x":159,"y":1377,"width":1231,"height":1261,"rx":96,"fill":163,"stroke":150,"style":1233},"192.0",[122,1379,1381],{"x":1264,"y":1380,"style":1266},"214.0","Secret manager",[93,1383],{"x":1236,"y":1377,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1385,1387],{"transform":1386},"translate(285.5,201.0) scale(0.667)",[115,1388],{"d":1284,"fill":118,"stroke":1285,"style":1286},[93,1390],{"x":1246,"y":1377,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1392,1394],{"transform":1393},"translate(463.1,200.0) scale(0.750)",[115,1395],{"d":1275,"fill":118,"stroke":232,"style":1276},[93,1397],{"x":1253,"y":1377,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1399,1401],{"transform":1400},"translate(641.7,200.0) scale(0.750)",[115,1402],{"d":1275,"fill":118,"stroke":232,"style":1276},[93,1404],{"x":159,"y":1405,"width":1231,"height":1261,"rx":96,"fill":163,"stroke":150,"style":1233},"226.0",[122,1407,274],{"x":1264,"y":1408,"style":1266},"248.0",[93,1410],{"x":1236,"y":1405,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[111,1412,1414],{"transform":1413},"translate(285.5,235.0) scale(0.667)",[115,1415],{"d":1284,"fill":118,"stroke":1285,"style":1286},[93,1417],{"x":1246,"y":1405,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[122,1419,1421],{"x":1249,"y":1420,"style":179},"247.0","no secret",[93,1423],{"x":1253,"y":1405,"width":1237,"height":1261,"rx":96,"fill":99,"stroke":150,"style":1233},[122,1425,1426],{"x":1256,"y":1420,"style":179},"platform-managed",[20,1428,1430],{"id":1429},"step-5-keep-secrets-out-of-logs","Step 5 — Keep secrets out of logs",[10,1432,1433],{},"The most common leak is not the code but the log: a connection string printed in a traceback, an exception message containing a URL with a password, a debug line printing the config. A logging filter that masks known secret values catches them wherever they appear:",[25,1435,1437],{"className":287,"code":1436,"language":289,"meta":30,"style":30},"import logging\n\nclass RedactSecrets(logging.Filter):\n    def __init__(self, secrets):\n        super().__init__()\n        self.secrets = [s for s in secrets if s and len(s) >= 6]\n    def filter(self, record):\n        message = record.getMessage()\n        for s in self.secrets:\n            message = message.replace(s, \"***\")\n        record.msg, record.args = message, ()\n        return True\n\nlog = logging.getLogger(\"weekly_report\")\nhandler = logging.StreamHandler()\nhandler.setFormatter(logging.Formatter(\"%(levelname)s %(message)s\"))\nhandler.addFilter(RedactSecrets(list(secrets.values()) + [db[\"password\"]]))\nlog.addHandler(handler)\nlog.setLevel(logging.INFO)\nlog.info(\"connecting to %s with password %s\", secrets[\"REPORT_DB_URL\"], secrets[\"REPORT_DB_PASSWORD\"])\nlog.error(\"SMTP login failed for reports@acme.example using %s\", secrets[\"SMTP_PASSWORD\"])\n",[32,1438,1439,1446,1450,1470,1479,1493,1536,1546,1556,1570,1585,1595,1602,1606,1621,1631,1648,1670,1675,1685,1717],{"__ignoreMap":30},[35,1440,1441,1443],{"class":37,"line":38},[35,1442,297],{"class":296},[35,1444,1445],{"class":300}," logging\n",[35,1447,1448],{"class":37,"line":58},[35,1449,307],{"emptyLinePlaceholder":306},[35,1451,1452,1454,1457,1459,1462,1465,1468],{"class":37,"line":310},[35,1453,348],{"class":296},[35,1455,1456],{"class":41}," RedactSecrets",[35,1458,354],{"class":300},[35,1460,1461],{"class":783},"logging",[35,1463,1464],{"class":300},".",[35,1466,1467],{"class":783},"Filter",[35,1469,360],{"class":300},[35,1471,1472,1474,1476],{"class":37,"line":340},[35,1473,801],{"class":296},[35,1475,804],{"class":313},[35,1477,1478],{"class":300},"(self, secrets):\n",[35,1480,1481,1484,1487,1490],{"class":37,"line":345},[35,1482,1483],{"class":313},"        super",[35,1485,1486],{"class":300},"().",[35,1488,1489],{"class":313},"__init__",[35,1491,1492],{"class":300},"()\n",[35,1494,1495,1497,1500,1502,1505,1507,1510,1512,1515,1517,1519,1522,1525,1528,1531,1534],{"class":37,"line":363},[35,1496,812],{"class":313},[35,1498,1499],{"class":300},".secrets ",[35,1501,387],{"class":296},[35,1503,1504],{"class":300}," [s ",[35,1506,405],{"class":296},[35,1508,1509],{"class":300}," s ",[35,1511,411],{"class":296},[35,1513,1514],{"class":300}," secrets ",[35,1516,417],{"class":296},[35,1518,1509],{"class":300},[35,1520,1521],{"class":296},"and",[35,1523,1524],{"class":313}," len",[35,1526,1527],{"class":300},"(s) ",[35,1529,1530],{"class":296},">=",[35,1532,1533],{"class":313}," 6",[35,1535,337],{"class":300},[35,1537,1538,1540,1543],{"class":37,"line":369},[35,1539,801],{"class":296},[35,1541,1542],{"class":313}," filter",[35,1544,1545],{"class":300},"(self, record):\n",[35,1547,1548,1551,1553],{"class":37,"line":374},[35,1549,1550],{"class":300},"        message ",[35,1552,387],{"class":296},[35,1554,1555],{"class":300}," record.getMessage()\n",[35,1557,1558,1561,1563,1565,1567],{"class":37,"line":394},[35,1559,1560],{"class":296},"        for",[35,1562,1509],{"class":300},[35,1564,411],{"class":296},[35,1566,860],{"class":313},[35,1568,1569],{"class":300},".secrets:\n",[35,1571,1572,1575,1577,1580,1583],{"class":37,"line":426},[35,1573,1574],{"class":300},"            message ",[35,1576,387],{"class":296},[35,1578,1579],{"class":300}," message.replace(s, ",[35,1581,1582],{"class":45},"\"***\"",[35,1584,466],{"class":300},[35,1586,1587,1590,1592],{"class":37,"line":435},[35,1588,1589],{"class":300},"        record.msg, record.args ",[35,1591,387],{"class":296},[35,1593,1594],{"class":300}," message, ()\n",[35,1596,1597,1599],{"class":37,"line":469},[35,1598,857],{"class":296},[35,1600,1601],{"class":313}," True\n",[35,1603,1604],{"class":37,"line":487},[35,1605,307],{"emptyLinePlaceholder":306},[35,1607,1608,1611,1613,1616,1619],{"class":37,"line":492},[35,1609,1610],{"class":300},"log ",[35,1612,387],{"class":296},[35,1614,1615],{"class":300}," logging.getLogger(",[35,1617,1618],{"class":45},"\"weekly_report\"",[35,1620,466],{"class":300},[35,1622,1623,1626,1628],{"class":37,"line":501},[35,1624,1625],{"class":300},"handler ",[35,1627,387],{"class":296},[35,1629,1630],{"class":300}," logging.StreamHandler()\n",[35,1632,1633,1636,1638,1641,1644,1646],{"class":37,"line":507},[35,1634,1635],{"class":300},"handler.setFormatter(logging.Formatter(",[35,1637,463],{"class":45},[35,1639,1640],{"class":450},"%(levelname)s",[35,1642,1643],{"class":450}," %(message)s",[35,1645,463],{"class":45},[35,1647,936],{"class":300},[35,1649,1650,1653,1656,1659,1662,1665,1667],{"class":37,"line":522},[35,1651,1652],{"class":300},"handler.addFilter(RedactSecrets(",[35,1654,1655],{"class":313},"list",[35,1657,1658],{"class":300},"(secrets.values()) ",[35,1660,1661],{"class":296},"+",[35,1663,1664],{"class":300}," [db[",[35,1666,1149],{"class":45},[35,1668,1669],{"class":300},"]]))\n",[35,1671,1672],{"class":37,"line":922},[35,1673,1674],{"class":300},"log.addHandler(handler)\n",[35,1676,1677,1680,1683],{"class":37,"line":939},[35,1678,1679],{"class":300},"log.setLevel(logging.",[35,1681,1682],{"class":313},"INFO",[35,1684,466],{"class":300},[35,1686,1688,1691,1694,1697,1700,1702,1704,1707,1709,1712,1714],{"class":37,"line":1687},20,[35,1689,1690],{"class":300},"log.info(",[35,1692,1693],{"class":45},"\"connecting to ",[35,1695,1696],{"class":450},"%s",[35,1698,1699],{"class":45}," with password ",[35,1701,1696],{"class":450},[35,1703,463],{"class":45},[35,1705,1706],{"class":300},", secrets[",[35,1708,323],{"class":45},[35,1710,1711],{"class":300},"], secrets[",[35,1713,329],{"class":45},[35,1715,1716],{"class":300},"])\n",[35,1718,1720,1723,1726,1728,1730,1732,1734],{"class":37,"line":1719},21,[35,1721,1722],{"class":300},"log.error(",[35,1724,1725],{"class":45},"\"SMTP login failed for reports@acme.example using ",[35,1727,1696],{"class":450},[35,1729,463],{"class":45},[35,1731,1706],{"class":300},[35,1733,334],{"class":45},[35,1735,1716],{"class":300},[74,1737,83,1742,83,1745,83,1748,83,1751,83,1755,83,1761,83,1765,83,1769,83,1774,83,1777,83,1780,83,1785,83,1788,83,1791,83,1795,83,1798,83,1801,83,1807,83,1811],{"viewBox":1738,"role":77,"ariaLabelledBy":1739,"xmlns":81,"style":82},"4 0 752 125",[1740,1741],"cr-redact-t","cr-redact-d",[85,1743,1744],{"id":1740},"A log record on its way to the file",[89,1746,1747],{"id":1741},"A log call builds a message containing the database password; the redaction filter attached to the handler replaces every known secret value with three asterisks; the handler then writes the masked line.",[93,1749],{"x":95,"y":96,"width":97,"height":1750,"fill":99},"125",[93,1752],{"x":159,"y":103,"width":1753,"height":1754,"rx":106,"fill":163,"stroke":150,"style":109},"216.0","93.0",[111,1756,1758],{"transform":1757},"translate(116.0,29.0) scale(1.000)",[115,1759],{"d":1760,"fill":118,"stroke":170,"style":120},"M9 8l-4 4 4 4 M15 8l4 4-4 4",[122,1762,1764],{"x":1763,"y":105,"style":174},"128.0","log.info(...)",[122,1766,1768],{"x":1763,"y":1767,"style":179},"95.0","message with the password",[37,1770],{"x1":1771,"y1":1772,"x2":1773,"y2":1772,"stroke":108,"style":109},"241.0","62.5","263.0",[139,1775],{"points":1776,"fill":142},"267.0,62.5 258.0,67.5 258.0,57.5",[93,1778],{"x":1779,"y":103,"width":1753,"height":1754,"rx":106,"fill":107,"stroke":108,"style":109},"272.0",[111,1781,1782],{"transform":113},[115,1783],{"d":1784,"fill":118,"stroke":119,"style":120},"M3 5h18l-7 8v6l-4 2v-8z",[122,1786,1787],{"x":124,"y":105,"style":125},"RedactSecrets",[122,1789,1790],{"x":124,"y":1767,"style":179},"replace known values → ***",[37,1792],{"x1":1793,"y1":1772,"x2":1794,"y2":1772,"stroke":108,"style":109},"493.0","515.0",[139,1796],{"points":1797,"fill":142},"519.0,62.5 510.0,67.5 510.0,57.5",[93,1799],{"x":1800,"y":103,"width":1753,"height":1754,"rx":106,"fill":231,"stroke":232,"style":109},"524.0",[111,1802,1804],{"transform":1803},"translate(620.0,29.0) scale(1.000)",[115,1805],{"d":1806,"fill":118,"stroke":239,"style":120},"M6 3h9l4 4v14H6z M15 3v4h4",[122,1808,1810],{"x":1809,"y":105,"style":242},"632.0","Handler writes",[122,1812,1813],{"x":1809,"y":1767,"style":179},"console, file, alert email",[10,1815,1816,1817,1820,1821,1825],{},"The filter replaces every configured secret value with ",[32,1818,1819],{},"***"," before the message is written, whichever handler writes it. Values shorter than six characters are skipped, because masking a two-character string would mangle ordinary text. The same filter belongs on file handlers — ",[14,1822,1824],{"href":1823},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File"," — and on the handler that emails failure alerts.",[20,1827,1829],{"id":1828},"common-pitfalls","Common pitfalls",[1831,1832,1833,1841,1853,1859,1865],"ul",{},[1834,1835,1836,1840],"li",{},[1837,1838,1839],"strong",{},"Passwords in the script \"just for testing\"."," They get committed; read them from the environment from day one.",[1834,1842,1843,1848,1849,1852],{},[1837,1844,1845,1846,1464],{},"Committing ",[32,1847,549],{}," Add it to ",[32,1850,1851],{},".gitignore"," before creating it, and scan history if it ever slipped in — a committed secret must be rotated, not just deleted.",[1834,1854,1855,1858],{},[1837,1856,1857],{},"Secrets in command-line arguments."," They are visible to other users in the process list; use the environment or a file readable only by the job.",[1834,1860,1861,1864],{},[1837,1862,1863],{},"Printing the config for debugging."," Log which variables are set, never their values.",[1834,1866,1867,1870],{},[1837,1868,1869],{},"Keyrings on servers."," Headless Linux usually has none; use the service's environment or a secret manager.",[20,1872,1874],{"id":1873},"conclusion","Conclusion",[10,1876,1877,1878,1880],{},"Read every secret through one function that checks the environment and names anything missing. Load a git-ignored ",[32,1879,549],{}," file on developer machines only, use the operating system keyring for desktop jobs, and on servers take secrets from the service's environment or a secret manager — or avoid them entirely with a managed identity. Add a logging filter that masks secret values so no traceback or debug line can leak them.",[20,1882,1884],{"id":1883},"frequently-asked-questions","Frequently asked questions",[10,1886,1887,1890],{},[1837,1888,1889],{},"Where should I store the database password for a Python report script?","\nNot in the script. Read it from an environment variable set by the scheduler or service, from the operating system keyring on a desktop, or from a secret manager on a server; locally, load it from a git-ignored .env file.",[10,1892,1893,1896],{},[1837,1894,1895],{},"Is a .env file safe?","\nIt is fine for development if it is excluded from version control, but it is still a plain-text file. On servers, use the service manager's environment settings or a secret manager instead.",[10,1898,1899,1902],{},[1837,1900,1901],{},"How do I use Windows Credential Manager or the macOS Keychain from Python?","\nUse the keyring library: keyring.set_password(service, username, password) once, and keyring.get_password(service, username) in the script. It selects the platform's secure store automatically.",[10,1904,1905,1908],{},[1837,1906,1907],{},"How do I stop passwords appearing in log files?","\nAdd a logging.Filter to every handler that replaces known secret values with a mask before the message is written, and avoid logging configuration objects or connection strings.",[20,1910,1912],{"id":1911},"related","Related",[1831,1914,1915,1921,1928,1935],{},[1834,1916,1917,1918,1920],{},"Up one level: ",[14,1919,17],{"href":16}," — making report code dependable.",[1834,1922,1923,1927],{},[14,1924,1926],{"href":1925},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","Keep Excel Report Settings in a Config File"," — non-secret settings belong here.",[1834,1929,1930,1934],{},[14,1931,1933],{"href":1932},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fauthenticate-to-microsoft-graph-with-msal-for-excel-automation\u002F","Authenticate to Microsoft Graph with MSAL for Excel Automation"," — app credentials for Microsoft 365.",[1834,1936,1937,1941],{},[14,1938,1940],{"href":1939},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fset-up-an-excel-automation-project-with-uv\u002F","Set Up an Excel Automation Project with uv"," — where .env.example and .gitignore live.",[1943,1944,1945],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sHg5J, html code.shiki .sHg5J{--shiki-default:#6F42C1;--shiki-dark:#91CBFF}",{"title":30,"searchDepth":58,"depth":58,"links":1947},[1948,1949,1950,1951,1952,1953,1954,1955,1956,1957],{"id":22,"depth":58,"text":23},{"id":280,"depth":58,"text":281},{"id":538,"depth":58,"text":539},{"id":735,"depth":58,"text":736},{"id":969,"depth":58,"text":970},{"id":1429,"depth":58,"text":1430},{"id":1828,"depth":58,"text":1829},{"id":1873,"depth":58,"text":1874},{"id":1883,"depth":58,"text":1884},{"id":1911,"depth":58,"text":1912},"2026-09-29","Get database passwords, SMTP logins and API keys out of report scripts: environment variables with a clear error when one is missing, a .env file for development only, the operating system keyring on desktops, a cloud secret manager for servers, identities that need no secret at all, and logs that never print a password.","md",[1962,1964,1966,1968],{"q":1889,"a":1963},"Not in the script. Read it from an environment variable set by the scheduler or service, from the operating system keyring on a desktop, or from a secret manager on a server; locally, load it from a git-ignored .env file.",{"q":1895,"a":1965},"It is fine for development if it is excluded from version control, but it is still a plain-text file. On servers, use the service manager's environment settings or a secret manager instead.",{"q":1901,"a":1967},"Use the keyring library: keyring.set_password(service, username, password) once, and keyring.get_password(service, username) in the script. It selects the platform's secure store automatically.",{"q":1907,"a":1969},"Add a logging.Filter to every handler that replaces known secret values with a mask before the message is written, and avoid logging configuration objects or connection strings.","store credentials for excel report scripts safely",null,{"breadcrumb":1973},[1974,1977,1980],{"name":1975,"item":1976},"Home","\u002F",{"name":1978,"item":1979},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely",{"title":1983,"description":1984},"Store Passwords and API Keys for Python Report Scripts","Keep credentials out of Python Excel report scripts: os.environ with required-variable checks, python-dotenv for local development, keyring for desktop jobs, AWS Secrets Manager with caching, managed identities, and a logging filter that redacts secrets.","store-credentials-for-excel-report-scripts-safely","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-credentials-for-excel-report-scripts-safely\u002Findex",[1988,1989],"reliable-jobs","email-delivery","how-to","3NmcQUQUETItBzZ4NysQ6dxKZgDEc2KkPfUPDh13jTk",[1993,1996],{"title":1940,"path":1994,"stem":1995,"children":-1},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fset-up-an-excel-automation-project-with-uv","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fset-up-an-excel-automation-project-with-uv\u002Findex",{"title":1997,"path":1998,"stem":1999,"children":-1},"Store Excel Files in Git with Git LFS","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-excel-files-in-git-with-git-lfs","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fstore-excel-files-in-git-with-git-lfs\u002Findex",[2001,2003],{"path":2002,"title":1978},"\u002Fautomating-reporting-workflows",{"path":2004,"title":17},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts",[],[2007,2011,2014,2017,2019,2022],{"path":2008,"title":2009,"kind":2010},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",{"path":2012,"title":2013,"kind":1990},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run","Detect a Scheduled Report That Did Not Run",{"path":2015,"title":2016,"kind":1990},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder","Ignore Excel Lock Files When Processing a Folder",{"path":2018,"title":1824,"kind":1990},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file",{"path":2020,"title":2021,"kind":1990},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fmake-an-excel-report-job-safe-to-rerun","Make an Excel Report Job Safe to Rerun",{"path":2023,"title":2024,"kind":1990},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports","Prevent Formula Injection in Excel Exports from Python",1790972142831]