[{"data":1,"prerenderedAt":1594},["ShallowReactive",2],{"post:\u002F2026\u002F09\u002F09\u002Fgoogle-cloud-iam-least-privilege-in-practice-not-just-in-theory\u002F":3},{"post":4,"newer":1544,"older":1556,"related":1567,"series":1592},{"id":5,"title":6,"body":7,"canonical":1528,"categories":1529,"date":1531,"description":1532,"extension":1533,"featured":1534,"hero":1535,"image":1535,"meta":1536,"navigation":453,"path":1537,"readingTime":264,"seo":1538,"series":1535,"seriesOrder":1535,"sites":1539,"source":1535,"stem":1540,"tags":1541,"updated":1535,"url":1542,"__hash__":1543},"blog\u002Fblog\u002F2026\u002F09\u002F09\u002Fgoogle-cloud-iam-least-privilege-in-practice-not-just-in-theory.md","Google Cloud: IAM Least Privilege in Practice, Not Just in Theory",{"type":8,"value":9,"toc":1519},"minimark",[10,14,19,22,30,98,107,152,155,159,166,173,205,208,311,359,362,366,385,388,403,995,998,1259,1270,1274,1281,1326,1329,1333,1336,1348,1351,1421,1429,1433,1436,1440,1515],[11,12,13],"p",{},"Least privilege is easy to agree with and hard to actually run. Every engineer nods along with the principle right up until their own access gets tightened and something they used to do without thinking now throws a permission error. Getting a Google Cloud organization to genuinely operate on least privilege, not just have it written into a policy document, has been less about picking the right roles and more about building the process that keeps them right after the initial cleanup. This is that process, with the commands and scripts behind each step.",[15,16,18],"h2",{"id":17},"where-least-privilege-breaks-down-in-practice","Where least privilege breaks down in practice",[11,20,21],{},"The gap almost never shows up at the design stage. It shows up months later, after the original access grants have drifted from whatever was documented, because someone needed a permission urgently, got a broad role to unblock them, and nobody came back to narrow it. Least privilege isn't a state you reach once. It's a maintenance discipline, and the organizations that struggle with it are the ones treating an access review as a one-time project instead of a recurring one.",[11,23,24,25,29],{},"The first recurring question is always the same: where are the basic roles? Google's own guidance is that basic roles \"include thousands of permissions across all Google Cloud services,\" and that in production you shouldn't grant them unless there's no alternative. Cloud Asset Inventory answers it for the whole organization in one call (you need ",[26,27,28],"code",{},"roles\u002Fcloudasset.viewer"," on the organization):",[31,32,37],"pre",{"className":33,"code":34,"language":35,"meta":36,"style":36},"language-bash shiki shiki-themes github-dark","gcloud asset search-all-iam-policies \\\n  --scope=organizations\u002F123456789012 \\\n  --query='roles:roles\u002Fowner OR roles:roles\u002Feditor' \\\n  --flatten='policy.bindings[].members[]' \\\n  --format='table(resource.basename(),policy.bindings.role,policy.bindings.members)'\n","bash","",[26,38,39,59,67,78,89],{"__ignoreMap":36},[40,41,44,48,52,55],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"svObZ","gcloud",[40,49,51],{"class":50},"sU2Wk"," asset",[40,53,54],{"class":50}," search-all-iam-policies",[40,56,58],{"class":57},"sDLfK"," \\\n",[40,60,62,65],{"class":42,"line":61},2,[40,63,64],{"class":57},"  --scope=organizations\u002F123456789012",[40,66,58],{"class":57},[40,68,70,73,76],{"class":42,"line":69},3,[40,71,72],{"class":57},"  --query=",[40,74,75],{"class":50},"'roles:roles\u002Fowner OR roles:roles\u002Feditor'",[40,77,58],{"class":57},[40,79,81,84,87],{"class":42,"line":80},4,[40,82,83],{"class":57},"  --flatten=",[40,85,86],{"class":50},"'policy.bindings[].members[]'",[40,88,58],{"class":57},[40,90,92,95],{"class":42,"line":91},5,[40,93,94],{"class":57},"  --format=",[40,96,97],{"class":50},"'table(resource.basename(),policy.bindings.role,policy.bindings.members)'\n",[11,99,100,101,106],{},"That search returns bindings attached directly to each resource. It doesn't return access inherited from a folder or the organization. For a full CSV with flags for public, external, and deleted principals, I use the ",[102,103,105],"a",{"href":104},"\u002F2026\u002F01\u002F14\u002Fpython-gcloud-audit-iam-bindings-across-a-gcp-organization\u002F","organization-wide IAM audit script",". For \"who can actually do this one dangerous thing here,\" inheritance and group membership included, Policy Analyzer is the right tool:",[31,108,110],{"className":33,"code":109,"language":35,"meta":36,"style":36},"gcloud asset analyze-iam-policy \\\n  --organization=123456789012 \\\n  --full-resource-name=\u002F\u002Fcloudresourcemanager.googleapis.com\u002Fprojects\u002Fpayments-prod \\\n  --permissions='resourcemanager.projects.setIamPolicy' \\\n  --expand-groups\n",[26,111,112,123,130,137,147],{"__ignoreMap":36},[40,113,114,116,118,121],{"class":42,"line":43},[40,115,47],{"class":46},[40,117,51],{"class":50},[40,119,120],{"class":50}," analyze-iam-policy",[40,122,58],{"class":57},[40,124,125,128],{"class":42,"line":61},[40,126,127],{"class":57},"  --organization=123456789012",[40,129,58],{"class":57},[40,131,132,135],{"class":42,"line":69},[40,133,134],{"class":57},"  --full-resource-name=\u002F\u002Fcloudresourcemanager.googleapis.com\u002Fprojects\u002Fpayments-prod",[40,136,58],{"class":57},[40,138,139,142,145],{"class":42,"line":80},[40,140,141],{"class":57},"  --permissions=",[40,143,144],{"class":50},"'resourcemanager.projects.setIamPolicy'",[40,146,58],{"class":57},[40,148,149],{"class":42,"line":91},[40,150,151],{"class":57},"  --expand-groups\n",[11,153,154],{},"Budget for that one: more than 20 Policy Analyzer queries per organization per day needs the Premium or Enterprise tier of Security Command Center.",[15,156,158],{"id":157},"predefined-roles-are-a-starting-point-not-a-destination","Predefined roles are a starting point, not a destination",[11,160,161,162,165],{},"Predefined roles are a huge improvement over granting Editor or Owner out of convenience, but most of them are still built for a category of user, not a specific job. A role like ",[26,163,164],{},"roles\u002Fcompute.admin"," grants far more than most engineers assigned to it use day to day.",[11,167,168,169,172],{},"Custom roles solve the precision problem but introduce a maintenance one. Google maintains predefined roles and adds permissions to them as services grow. It doesn't touch your custom roles, so a custom role nobody has updated in two years is quietly falling behind what the underlying service can do. Some permissions also can't go into custom roles at all, and others are only at ",[26,170,171],{},"TESTING"," support level. Check before you build one:",[31,174,176],{"className":33,"code":175,"language":35,"meta":36,"style":36},"gcloud iam list-testable-permissions \\\n  \u002F\u002Fcloudresourcemanager.googleapis.com\u002Fprojects\u002Fpayments-prod \\\n  --filter='customRolesSupportLevel!=NOT_SUPPORTED AND name:compute.instances.'\n",[26,177,178,190,197],{"__ignoreMap":36},[40,179,180,182,185,188],{"class":42,"line":43},[40,181,47],{"class":46},[40,183,184],{"class":50}," iam",[40,186,187],{"class":50}," list-testable-permissions",[40,189,58],{"class":57},[40,191,192,195],{"class":42,"line":61},[40,193,194],{"class":50},"  \u002F\u002Fcloudresourcemanager.googleapis.com\u002Fprojects\u002Fpayments-prod",[40,196,58],{"class":57},[40,198,199,202],{"class":42,"line":69},[40,200,201],{"class":57},"  --filter=",[40,203,204],{"class":50},"'customRolesSupportLevel!=NOT_SUPPORTED AND name:compute.instances.'\n",[11,206,207],{},"When a custom role is worth it, keep it as a file in version control with a named owner, so the review has something to diff:",[31,209,213],{"className":210,"code":211,"language":212,"meta":36,"style":36},"language-yaml shiki shiki-themes github-dark","# roles\u002Fvm-operator.yaml\ntitle: VM Operator\ndescription: Start, stop, reset and view VMs. Owner platform-team@example.com.\nstage: GA\nincludedPermissions:\n- compute.instances.get\n- compute.instances.list\n- compute.instances.reset\n- compute.instances.start\n- compute.instances.stop\n- compute.zoneOperations.get\n","yaml",[26,214,215,221,234,244,254,262,271,279,287,295,303],{"__ignoreMap":36},[40,216,217],{"class":42,"line":43},[40,218,220],{"class":219},"sAwPA","# roles\u002Fvm-operator.yaml\n",[40,222,223,227,231],{"class":42,"line":61},[40,224,226],{"class":225},"s4JwU","title",[40,228,230],{"class":229},"s95oV",": ",[40,232,233],{"class":50},"VM Operator\n",[40,235,236,239,241],{"class":42,"line":69},[40,237,238],{"class":225},"description",[40,240,230],{"class":229},[40,242,243],{"class":50},"Start, stop, reset and view VMs. Owner platform-team@example.com.\n",[40,245,246,249,251],{"class":42,"line":80},[40,247,248],{"class":225},"stage",[40,250,230],{"class":229},[40,252,253],{"class":50},"GA\n",[40,255,256,259],{"class":42,"line":91},[40,257,258],{"class":225},"includedPermissions",[40,260,261],{"class":229},":\n",[40,263,265,268],{"class":42,"line":264},6,[40,266,267],{"class":229},"- ",[40,269,270],{"class":50},"compute.instances.get\n",[40,272,274,276],{"class":42,"line":273},7,[40,275,267],{"class":229},[40,277,278],{"class":50},"compute.instances.list\n",[40,280,282,284],{"class":42,"line":281},8,[40,283,267],{"class":229},[40,285,286],{"class":50},"compute.instances.reset\n",[40,288,290,292],{"class":42,"line":289},9,[40,291,267],{"class":229},[40,293,294],{"class":50},"compute.instances.start\n",[40,296,298,300],{"class":42,"line":297},10,[40,299,267],{"class":229},[40,301,302],{"class":50},"compute.instances.stop\n",[40,304,306,308],{"class":42,"line":305},11,[40,307,267],{"class":229},[40,309,310],{"class":50},"compute.zoneOperations.get\n",[31,312,314],{"className":33,"code":313,"language":35,"meta":36,"style":36},"gcloud iam roles create vmOperator --organization=123456789012 --file=roles\u002Fvm-operator.yaml\n# later changes\ngcloud iam roles update vmOperator --organization=123456789012 --file=roles\u002Fvm-operator.yaml\n",[26,315,316,337,342],{"__ignoreMap":36},[40,317,318,320,322,325,328,331,334],{"class":42,"line":43},[40,319,47],{"class":46},[40,321,184],{"class":50},[40,323,324],{"class":50}," roles",[40,326,327],{"class":50}," create",[40,329,330],{"class":50}," vmOperator",[40,332,333],{"class":57}," --organization=123456789012",[40,335,336],{"class":57}," --file=roles\u002Fvm-operator.yaml\n",[40,338,339],{"class":42,"line":61},[40,340,341],{"class":219},"# later changes\n",[40,343,344,346,348,350,353,355,357],{"class":42,"line":69},[40,345,47],{"class":46},[40,347,184],{"class":50},[40,349,324],{"class":50},[40,351,352],{"class":50}," update",[40,354,330],{"class":50},[40,356,333],{"class":57},[40,358,336],{"class":57},[11,360,361],{},"The middle ground we've landed on: predefined roles at the project or folder level for anything broad and low-risk, and custom roles reserved for the handful of high-privilege paths worth the ongoing upkeep. (The hard limit is 3,000 permissions per custom role; if you're anywhere near it, a predefined role was probably the better answer.)",[15,363,365],{"id":364},"the-iam-recommender-is-only-as-good-as-its-lookback-window","The IAM recommender is only as good as its lookback window",[11,367,368,369,372,373,376,377,380,381,384],{},"The IAM recommender (",[26,370,371],{},"google.iam.policy.Recommender",") is the fastest way to find over-provisioned bindings. It compares what a principal was granted with what it used over up to 90 days (or since the grant, if that's more recent), and it proposes one of two things: ",[26,374,375],{},"REMOVE_ROLE",", or ",[26,378,379],{},"REPLACE_ROLE"," with something less permissive. There's also ",[26,382,383],{},"REPLACE_ROLE_CUSTOMIZABLE",", a suggested custom role. It also uses an ML model to keep permissions a principal is likely to need even if it hasn't used them yet. Recommendations for basic roles are free at the project, folder, and organization level. Recommendations for other predefined roles and custom-role suggestions need Security Command Center Premium or Enterprise. It also doesn't produce insights for conditional bindings.",[11,386,387],{},"What it can't tell you is whether an unused permission is unused because it's unnecessary, or because the one task that needs it only runs quarterly and didn't fall inside the window. Accepting every recommendation blind is how you find out, at the worst possible time, that someone's quarter-end batch job needed a permission the recommender flagged as safe to remove. We treat recommendations as a prioritized list to investigate, not a queue to auto-apply.",[11,389,390,391,394,395,398,399,402],{},"This script builds that list across every project the caller can see, together with the related list of service accounts that haven't authenticated in 90 days. It needs ",[26,392,393],{},"roles\u002Frecommender.iamViewer"," and ",[26,396,397],{},"roles\u002Fiam.roleViewer"," wherever it runs (granting them at the organization covers every project), plus the Recommender API (",[26,400,401],{},"recommender.googleapis.com",") enabled.",[31,404,406],{"className":33,"code":405,"language":35,"meta":36,"style":36},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# iam-review.sh\n# Collects active IAM role recommendations and unused-service-account insights\n# for every visible project into two CSV files for a periodic access review.\n# Usage: .\u002Fiam-review.sh [PROJECT_FILTER]\n#   PROJECT_FILTER is an optional gcloud --filter, default \"lifecycleState=ACTIVE\"\nset -euo pipefail\n\nFILTER=\"${1:-lifecycleState=ACTIVE}\"\nREC_CSV=\"iam-recommendations-$(date +%F).csv\"\nSA_CSV=\"unused-service-accounts-$(date +%F).csv\"\n\necho \"project,recommendation_id,subtype,priority,etag,description\" > \"$REC_CSV\"\necho \"project,service_account,subtype,severity,description\" > \"$SA_CSV\"\n\nfor project in $(gcloud projects list --filter=\"$FILTER\" --format='value(projectId)'); do\n  echo \"Reviewing $project\" >&2\n\n  if ! recs=$(gcloud recommender recommendations list \\\n      --project=\"$project\" --location=global \\\n      --recommender=google.iam.policy.Recommender \\\n      --filter='stateInfo.state=ACTIVE' \\\n      --format='csv[no-heading](name.basename(),recommenderSubtype,priority,etag,description)' 2>\u002Fdev\u002Fnull); then\n    echo \"  skipped recommendations (API disabled or no access)\" >&2\n    recs=\"\"\n  fi\n  [[ -n \"$recs\" ]] && sed \"s|^|${project},|\" \u003C\u003C\u003C \"$recs\" >> \"$REC_CSV\"\n\n  if ! sas=$(gcloud recommender insights list \\\n      --project=\"$project\" --location=global \\\n      --insight-type=google.iam.serviceAccount.Insight \\\n      --filter='stateInfo.state=ACTIVE' \\\n      --format='csv[no-heading](targetResources[0].basename(),insightSubtype,severity,description)' 2>\u002Fdev\u002Fnull); then\n    echo \"  skipped service account insights\" >&2\n    sas=\"\"\n  fi\n  [[ -n \"$sas\" ]] && sed \"s|^|${project},|\" \u003C\u003C\u003C \"$sas\" >> \"$SA_CSV\"\ndone\n\necho \"Wrote $REC_CSV ($(($(wc -l \u003C \"$REC_CSV\") - 1)) rows) and $SA_CSV ($(($(wc -l \u003C \"$SA_CSV\") - 1)) rows)\" >&2\n",[26,407,408,413,418,423,428,433,438,449,455,487,503,517,522,542,559,564,609,626,631,660,677,685,696,716,727,738,744,793,798,823,838,846,855,871,881,891,896,936,942,947],{"__ignoreMap":36},[40,409,410],{"class":42,"line":43},[40,411,412],{"class":219},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[40,414,415],{"class":42,"line":61},[40,416,417],{"class":219},"# iam-review.sh\n",[40,419,420],{"class":42,"line":69},[40,421,422],{"class":219},"# Collects active IAM role recommendations and unused-service-account insights\n",[40,424,425],{"class":42,"line":80},[40,426,427],{"class":219},"# for every visible project into two CSV files for a periodic access review.\n",[40,429,430],{"class":42,"line":91},[40,431,432],{"class":219},"# Usage: .\u002Fiam-review.sh [PROJECT_FILTER]\n",[40,434,435],{"class":42,"line":264},[40,436,437],{"class":219},"#   PROJECT_FILTER is an optional gcloud --filter, default \"lifecycleState=ACTIVE\"\n",[40,439,440,443,446],{"class":42,"line":273},[40,441,442],{"class":57},"set",[40,444,445],{"class":57}," -euo",[40,447,448],{"class":50}," pipefail\n",[40,450,451],{"class":42,"line":281},[40,452,454],{"emptyLinePlaceholder":453},true,"\n",[40,456,457,460,464,467,470,473,476,478,481,484],{"class":42,"line":289},[40,458,459],{"class":229},"FILTER",[40,461,463],{"class":462},"snl16","=",[40,465,466],{"class":50},"\"",[40,468,469],{"class":57},"${1",[40,471,472],{"class":462},":-",[40,474,475],{"class":229},"lifecycleState",[40,477,463],{"class":50},[40,479,480],{"class":229},"ACTIVE",[40,482,483],{"class":57},"}",[40,485,486],{"class":50},"\"\n",[40,488,489,492,494,497,500],{"class":42,"line":297},[40,490,491],{"class":229},"REC_CSV",[40,493,463],{"class":462},[40,495,496],{"class":50},"\"iam-recommendations-$(",[40,498,499],{"class":46},"date",[40,501,502],{"class":50}," +%F).csv\"\n",[40,504,505,508,510,513,515],{"class":42,"line":305},[40,506,507],{"class":229},"SA_CSV",[40,509,463],{"class":462},[40,511,512],{"class":50},"\"unused-service-accounts-$(",[40,514,499],{"class":46},[40,516,502],{"class":50},[40,518,520],{"class":42,"line":519},12,[40,521,454],{"emptyLinePlaceholder":453},[40,523,525,528,531,534,537,540],{"class":42,"line":524},13,[40,526,527],{"class":57},"echo",[40,529,530],{"class":50}," \"project,recommendation_id,subtype,priority,etag,description\"",[40,532,533],{"class":462}," >",[40,535,536],{"class":50}," \"",[40,538,539],{"class":229},"$REC_CSV",[40,541,486],{"class":50},[40,543,545,547,550,552,554,557],{"class":42,"line":544},14,[40,546,527],{"class":57},[40,548,549],{"class":50}," \"project,service_account,subtype,severity,description\"",[40,551,533],{"class":462},[40,553,536],{"class":50},[40,555,556],{"class":229},"$SA_CSV",[40,558,486],{"class":50},[40,560,562],{"class":42,"line":561},15,[40,563,454],{"emptyLinePlaceholder":453},[40,565,567,570,573,576,579,581,584,587,590,592,595,597,600,603,606],{"class":42,"line":566},16,[40,568,569],{"class":462},"for",[40,571,572],{"class":229}," project ",[40,574,575],{"class":462},"in",[40,577,578],{"class":229}," $(",[40,580,47],{"class":46},[40,582,583],{"class":50}," projects",[40,585,586],{"class":50}," list",[40,588,589],{"class":57}," --filter=",[40,591,466],{"class":50},[40,593,594],{"class":229},"$FILTER",[40,596,466],{"class":50},[40,598,599],{"class":57}," --format=",[40,601,602],{"class":50},"'value(projectId)'",[40,604,605],{"class":229},"); ",[40,607,608],{"class":462},"do\n",[40,610,612,615,618,621,623],{"class":42,"line":611},17,[40,613,614],{"class":57},"  echo",[40,616,617],{"class":50}," \"Reviewing ",[40,619,620],{"class":229},"$project",[40,622,466],{"class":50},[40,624,625],{"class":462}," >&2\n",[40,627,629],{"class":42,"line":628},18,[40,630,454],{"emptyLinePlaceholder":453},[40,632,634,637,640,643,645,648,650,653,656,658],{"class":42,"line":633},19,[40,635,636],{"class":462},"  if",[40,638,639],{"class":462}," !",[40,641,642],{"class":229}," recs",[40,644,463],{"class":462},[40,646,647],{"class":229},"$(",[40,649,47],{"class":46},[40,651,652],{"class":50}," recommender",[40,654,655],{"class":50}," recommendations",[40,657,586],{"class":50},[40,659,58],{"class":57},[40,661,663,666,668,670,672,675],{"class":42,"line":662},20,[40,664,665],{"class":57},"      --project=",[40,667,466],{"class":50},[40,669,620],{"class":229},[40,671,466],{"class":50},[40,673,674],{"class":57}," --location=global",[40,676,58],{"class":57},[40,678,680,683],{"class":42,"line":679},21,[40,681,682],{"class":57},"      --recommender=google.iam.policy.Recommender",[40,684,58],{"class":57},[40,686,688,691,694],{"class":42,"line":687},22,[40,689,690],{"class":57},"      --filter=",[40,692,693],{"class":50},"'stateInfo.state=ACTIVE'",[40,695,58],{"class":57},[40,697,699,702,705,708,711,713],{"class":42,"line":698},23,[40,700,701],{"class":57},"      --format=",[40,703,704],{"class":50},"'csv[no-heading](name.basename(),recommenderSubtype,priority,etag,description)'",[40,706,707],{"class":462}," 2>",[40,709,710],{"class":50},"\u002Fdev\u002Fnull",[40,712,605],{"class":229},[40,714,715],{"class":462},"then\n",[40,717,719,722,725],{"class":42,"line":718},24,[40,720,721],{"class":57},"    echo",[40,723,724],{"class":50}," \"  skipped recommendations (API disabled or no access)\"",[40,726,625],{"class":462},[40,728,730,733,735],{"class":42,"line":729},25,[40,731,732],{"class":229},"    recs",[40,734,463],{"class":462},[40,736,737],{"class":50},"\"\"\n",[40,739,741],{"class":42,"line":740},26,[40,742,743],{"class":462},"  fi\n",[40,745,747,750,753,755,758,760,763,766,769,772,775,778,780,782,784,787,789,791],{"class":42,"line":746},27,[40,748,749],{"class":229},"  [[ ",[40,751,752],{"class":462},"-n",[40,754,536],{"class":50},[40,756,757],{"class":229},"$recs",[40,759,466],{"class":50},[40,761,762],{"class":229}," ]] && ",[40,764,765],{"class":46},"sed",[40,767,768],{"class":50}," \"s|^|${",[40,770,771],{"class":229},"project",[40,773,774],{"class":50},"},|\"",[40,776,777],{"class":462}," \u003C\u003C\u003C",[40,779,536],{"class":50},[40,781,757],{"class":229},[40,783,466],{"class":50},[40,785,786],{"class":462}," >>",[40,788,536],{"class":50},[40,790,539],{"class":229},[40,792,486],{"class":50},[40,794,796],{"class":42,"line":795},28,[40,797,454],{"emptyLinePlaceholder":453},[40,799,801,803,805,808,810,812,814,816,819,821],{"class":42,"line":800},29,[40,802,636],{"class":462},[40,804,639],{"class":462},[40,806,807],{"class":229}," sas",[40,809,463],{"class":462},[40,811,647],{"class":229},[40,813,47],{"class":46},[40,815,652],{"class":50},[40,817,818],{"class":50}," insights",[40,820,586],{"class":50},[40,822,58],{"class":57},[40,824,826,828,830,832,834,836],{"class":42,"line":825},30,[40,827,665],{"class":57},[40,829,466],{"class":50},[40,831,620],{"class":229},[40,833,466],{"class":50},[40,835,674],{"class":57},[40,837,58],{"class":57},[40,839,841,844],{"class":42,"line":840},31,[40,842,843],{"class":57},"      --insight-type=google.iam.serviceAccount.Insight",[40,845,58],{"class":57},[40,847,849,851,853],{"class":42,"line":848},32,[40,850,690],{"class":57},[40,852,693],{"class":50},[40,854,58],{"class":57},[40,856,858,860,863,865,867,869],{"class":42,"line":857},33,[40,859,701],{"class":57},[40,861,862],{"class":50},"'csv[no-heading](targetResources[0].basename(),insightSubtype,severity,description)'",[40,864,707],{"class":462},[40,866,710],{"class":50},[40,868,605],{"class":229},[40,870,715],{"class":462},[40,872,874,876,879],{"class":42,"line":873},34,[40,875,721],{"class":57},[40,877,878],{"class":50}," \"  skipped service account insights\"",[40,880,625],{"class":462},[40,882,884,887,889],{"class":42,"line":883},35,[40,885,886],{"class":229},"    sas",[40,888,463],{"class":462},[40,890,737],{"class":50},[40,892,894],{"class":42,"line":893},36,[40,895,743],{"class":462},[40,897,899,901,903,905,908,910,912,914,916,918,920,922,924,926,928,930,932,934],{"class":42,"line":898},37,[40,900,749],{"class":229},[40,902,752],{"class":462},[40,904,536],{"class":50},[40,906,907],{"class":229},"$sas",[40,909,466],{"class":50},[40,911,762],{"class":229},[40,913,765],{"class":46},[40,915,768],{"class":50},[40,917,771],{"class":229},[40,919,774],{"class":50},[40,921,777],{"class":462},[40,923,536],{"class":50},[40,925,907],{"class":229},[40,927,466],{"class":50},[40,929,786],{"class":462},[40,931,536],{"class":50},[40,933,556],{"class":229},[40,935,486],{"class":50},[40,937,939],{"class":42,"line":938},38,[40,940,941],{"class":462},"done\n",[40,943,945],{"class":42,"line":944},39,[40,946,454],{"emptyLinePlaceholder":453},[40,948,950,952,955,957,960,963,966,969,971,973,976,978,980,982,984,986,988,990,993],{"class":42,"line":949},40,[40,951,527],{"class":57},[40,953,954],{"class":50}," \"Wrote ",[40,956,539],{"class":229},[40,958,959],{"class":50}," ($(($(",[40,961,962],{"class":46},"wc",[40,964,965],{"class":57}," -l",[40,967,968],{"class":462}," \u003C",[40,970,536],{"class":50},[40,972,539],{"class":229},[40,974,975],{"class":50},"\") - 1)) rows) and ",[40,977,556],{"class":229},[40,979,959],{"class":50},[40,981,962],{"class":46},[40,983,965],{"class":57},[40,985,968],{"class":462},[40,987,536],{"class":50},[40,989,556],{"class":229},[40,991,992],{"class":50},"\") - 1)) rows)\"",[40,994,625],{"class":462},[11,996,997],{},"Working a row from that file looks like this. Claiming the recommendation stops the Recommender API from rewriting it while you act, and marking it succeeded closes the loop in the console:",[31,999,1001],{"className":33,"code":1000,"language":35,"meta":36,"style":36},"PROJECT=payments-prod\nREC_ID=0f1e2d3c-example\nETAG=$(gcloud recommender recommendations describe \"$REC_ID\" --project=\"$PROJECT\" \\\n  --location=global --recommender=google.iam.policy.Recommender --format='value(etag)')\n\ngcloud recommender recommendations mark-claimed \"$REC_ID\" --project=\"$PROJECT\" \\\n  --location=global --recommender=google.iam.policy.Recommender --etag=\"$ETAG\" \\\n  --state-metadata=reviewer=platform-team,ticket=SEC-1234\n\n# Apply the change the recommendation describes, for example:\ngcloud projects remove-iam-policy-binding \"$PROJECT\" \\\n  --member='user:jane@example.com' --role='roles\u002Feditor'\n\nETAG=$(gcloud recommender recommendations describe \"$REC_ID\" --project=\"$PROJECT\" \\\n  --location=global --recommender=google.iam.policy.Recommender --format='value(etag)')\ngcloud recommender recommendations mark-succeeded \"$REC_ID\" --project=\"$PROJECT\" \\\n  --location=global --recommender=google.iam.policy.Recommender --etag=\"$ETAG\"\n",[26,1002,1003,1013,1023,1060,1076,1080,1107,1125,1130,1134,1139,1156,1170,1174,1206,1218,1245],{"__ignoreMap":36},[40,1004,1005,1008,1010],{"class":42,"line":43},[40,1006,1007],{"class":229},"PROJECT",[40,1009,463],{"class":462},[40,1011,1012],{"class":50},"payments-prod\n",[40,1014,1015,1018,1020],{"class":42,"line":61},[40,1016,1017],{"class":229},"REC_ID",[40,1019,463],{"class":462},[40,1021,1022],{"class":50},"0f1e2d3c-example\n",[40,1024,1025,1028,1030,1032,1034,1036,1038,1041,1043,1046,1048,1051,1053,1056,1058],{"class":42,"line":69},[40,1026,1027],{"class":229},"ETAG",[40,1029,463],{"class":462},[40,1031,647],{"class":229},[40,1033,47],{"class":46},[40,1035,652],{"class":50},[40,1037,655],{"class":50},[40,1039,1040],{"class":50}," describe",[40,1042,536],{"class":50},[40,1044,1045],{"class":229},"$REC_ID",[40,1047,466],{"class":50},[40,1049,1050],{"class":57}," --project=",[40,1052,466],{"class":50},[40,1054,1055],{"class":229},"$PROJECT",[40,1057,466],{"class":50},[40,1059,58],{"class":57},[40,1061,1062,1065,1068,1070,1073],{"class":42,"line":80},[40,1063,1064],{"class":57},"  --location=global",[40,1066,1067],{"class":57}," --recommender=google.iam.policy.Recommender",[40,1069,599],{"class":57},[40,1071,1072],{"class":50},"'value(etag)'",[40,1074,1075],{"class":229},")\n",[40,1077,1078],{"class":42,"line":91},[40,1079,454],{"emptyLinePlaceholder":453},[40,1081,1082,1084,1086,1088,1091,1093,1095,1097,1099,1101,1103,1105],{"class":42,"line":264},[40,1083,47],{"class":46},[40,1085,652],{"class":50},[40,1087,655],{"class":50},[40,1089,1090],{"class":50}," mark-claimed",[40,1092,536],{"class":50},[40,1094,1045],{"class":229},[40,1096,466],{"class":50},[40,1098,1050],{"class":57},[40,1100,466],{"class":50},[40,1102,1055],{"class":229},[40,1104,466],{"class":50},[40,1106,58],{"class":57},[40,1108,1109,1111,1113,1116,1118,1121,1123],{"class":42,"line":273},[40,1110,1064],{"class":57},[40,1112,1067],{"class":57},[40,1114,1115],{"class":57}," --etag=",[40,1117,466],{"class":50},[40,1119,1120],{"class":229},"$ETAG",[40,1122,466],{"class":50},[40,1124,58],{"class":57},[40,1126,1127],{"class":42,"line":281},[40,1128,1129],{"class":57},"  --state-metadata=reviewer=platform-team,ticket=SEC-1234\n",[40,1131,1132],{"class":42,"line":289},[40,1133,454],{"emptyLinePlaceholder":453},[40,1135,1136],{"class":42,"line":297},[40,1137,1138],{"class":219},"# Apply the change the recommendation describes, for example:\n",[40,1140,1141,1143,1145,1148,1150,1152,1154],{"class":42,"line":305},[40,1142,47],{"class":46},[40,1144,583],{"class":50},[40,1146,1147],{"class":50}," remove-iam-policy-binding",[40,1149,536],{"class":50},[40,1151,1055],{"class":229},[40,1153,466],{"class":50},[40,1155,58],{"class":57},[40,1157,1158,1161,1164,1167],{"class":42,"line":519},[40,1159,1160],{"class":57},"  --member=",[40,1162,1163],{"class":50},"'user:jane@example.com'",[40,1165,1166],{"class":57}," --role=",[40,1168,1169],{"class":50},"'roles\u002Feditor'\n",[40,1171,1172],{"class":42,"line":524},[40,1173,454],{"emptyLinePlaceholder":453},[40,1175,1176,1178,1180,1182,1184,1186,1188,1190,1192,1194,1196,1198,1200,1202,1204],{"class":42,"line":544},[40,1177,1027],{"class":229},[40,1179,463],{"class":462},[40,1181,647],{"class":229},[40,1183,47],{"class":46},[40,1185,652],{"class":50},[40,1187,655],{"class":50},[40,1189,1040],{"class":50},[40,1191,536],{"class":50},[40,1193,1045],{"class":229},[40,1195,466],{"class":50},[40,1197,1050],{"class":57},[40,1199,466],{"class":50},[40,1201,1055],{"class":229},[40,1203,466],{"class":50},[40,1205,58],{"class":57},[40,1207,1208,1210,1212,1214,1216],{"class":42,"line":561},[40,1209,1064],{"class":57},[40,1211,1067],{"class":57},[40,1213,599],{"class":57},[40,1215,1072],{"class":50},[40,1217,1075],{"class":229},[40,1219,1220,1222,1224,1226,1229,1231,1233,1235,1237,1239,1241,1243],{"class":42,"line":566},[40,1221,47],{"class":46},[40,1223,652],{"class":50},[40,1225,655],{"class":50},[40,1227,1228],{"class":50}," mark-succeeded",[40,1230,536],{"class":50},[40,1232,1045],{"class":229},[40,1234,466],{"class":50},[40,1236,1050],{"class":57},[40,1238,466],{"class":50},[40,1240,1055],{"class":229},[40,1242,466],{"class":50},[40,1244,58],{"class":57},[40,1246,1247,1249,1251,1253,1255,1257],{"class":42,"line":611},[40,1248,1064],{"class":57},[40,1250,1067],{"class":57},[40,1252,1115],{"class":57},[40,1254,466],{"class":50},[40,1256,1120],{"class":229},[40,1258,486],{"class":50},[11,1260,1261,1262,1265,1266,1269],{},"Applying a recommendation needs ",[26,1263,1264],{},"roles\u002Frecommender.iamAdmin"," plus the right to change the resource's allow policy. Some project-level recommendations propose a new custom role, which also needs ",[26,1267,1268],{},"iam.roles.create",".",[15,1271,1273],{"id":1272},"make-broad-access-temporary-instead-of-permanent","Make broad access temporary instead of permanent",[11,1275,1276,1277,1280],{},"Most \"just give me Editor\" requests are really \"give me more access for this afternoon.\" IAM conditions make that expire on its own. A binding with a ",[26,1278,1279],{},"request.time"," condition stops granting access at the timestamp, and nobody has to remember to remove it:",[31,1282,1284],{"className":33,"code":1283,"language":35,"meta":36,"style":36},"gcloud projects add-iam-policy-binding payments-prod \\\n  --member='user:jane@example.com' \\\n  --role='roles\u002Fcompute.instanceAdmin.v1' \\\n  --condition='expression=request.time \u003C timestamp(\"2026-09-12T00:00:00Z\"),title=incident-4711,description=Temporary access for incident 4711'\n",[26,1285,1286,1300,1308,1318],{"__ignoreMap":36},[40,1287,1288,1290,1292,1295,1298],{"class":42,"line":43},[40,1289,47],{"class":46},[40,1291,583],{"class":50},[40,1293,1294],{"class":50}," add-iam-policy-binding",[40,1296,1297],{"class":50}," payments-prod",[40,1299,58],{"class":57},[40,1301,1302,1304,1306],{"class":42,"line":61},[40,1303,1160],{"class":57},[40,1305,1163],{"class":50},[40,1307,58],{"class":57},[40,1309,1310,1313,1316],{"class":42,"line":69},[40,1311,1312],{"class":57},"  --role=",[40,1314,1315],{"class":50},"'roles\u002Fcompute.instanceAdmin.v1'",[40,1317,58],{"class":57},[40,1319,1320,1323],{"class":42,"line":80},[40,1321,1322],{"class":57},"  --condition=",[40,1324,1325],{"class":50},"'expression=request.time \u003C timestamp(\"2026-09-12T00:00:00Z\"),title=incident-4711,description=Temporary access for incident 4711'\n",[11,1327,1328],{},"One limitation pushes you toward predefined roles here anyway: IAM refuses conditions on the basic roles (Owner, Editor, Viewer). For requests that need approval and an audit trail, Privileged Access Manager formalizes the same idea with entitlements that principals request and approvers grant for a bounded duration.",[15,1330,1332],{"id":1331},"service-accounts-are-the-real-risk","Service accounts are the real risk",[11,1334,1335],{},"Human access gets reviewed because humans are visible: they're in the org chart, they leave the company, someone notices. Service accounts have none of that natural pressure. A service account with a long-lived downloaded key and a broad role is a far bigger blast radius than any single over-permissioned engineer, because it's usually forgotten faster than it's found.",[11,1337,1338,1339,1342,1343,1347],{},"Google's key-management guidance is to use an alternative \"whenever possible\": an attached service account for workloads on Google Cloud, and Workload Identity Federation for workloads elsewhere (GitHub Actions, AWS, Azure, any OIDC or SAML provider), which authenticates without a downloaded key at all. The organization policy ",[26,1340,1341],{},"iam.managed.disableServiceAccountKeyCreation"," enforces that. It's already on by default for organizations created on or after May 3, 2024, and the ",[102,1344,1346],{"href":1345},"\u002F2026\u002F05\u002F13\u002Fgoogle-cloud-organization-policy-constraints-that-actually-matter\u002F","organization policy post"," covers rolling it out on older ones.",[11,1349,1350],{},"For the keys that remain, two checks come before any deletion:",[31,1352,1354],{"className":33,"code":1353,"language":35,"meta":36,"style":36},"# When was each key last used? (roles\u002Fpolicyanalyzer.activityAnalysisViewer)\ngcloud policy-intelligence query-activity \\\n  --activity-type=serviceAccountKeyLastAuthentication \\\n  --project=payments-prod\n\n# Which service accounts haven't authenticated in 90 days?\ngcloud recommender insights list \\\n  --insight-type=google.iam.serviceAccount.Insight \\\n  --project=payments-prod --location=global\n",[26,1355,1356,1361,1373,1380,1385,1389,1394,1406,1413],{"__ignoreMap":36},[40,1357,1358],{"class":42,"line":43},[40,1359,1360],{"class":219},"# When was each key last used? (roles\u002Fpolicyanalyzer.activityAnalysisViewer)\n",[40,1362,1363,1365,1368,1371],{"class":42,"line":61},[40,1364,47],{"class":46},[40,1366,1367],{"class":50}," policy-intelligence",[40,1369,1370],{"class":50}," query-activity",[40,1372,58],{"class":57},[40,1374,1375,1378],{"class":42,"line":69},[40,1376,1377],{"class":57},"  --activity-type=serviceAccountKeyLastAuthentication",[40,1379,58],{"class":57},[40,1381,1382],{"class":42,"line":80},[40,1383,1384],{"class":57},"  --project=payments-prod\n",[40,1386,1387],{"class":42,"line":91},[40,1388,454],{"emptyLinePlaceholder":453},[40,1390,1391],{"class":42,"line":264},[40,1392,1393],{"class":219},"# Which service accounts haven't authenticated in 90 days?\n",[40,1395,1396,1398,1400,1402,1404],{"class":42,"line":273},[40,1397,47],{"class":46},[40,1399,652],{"class":50},[40,1401,818],{"class":50},[40,1403,586],{"class":50},[40,1405,58],{"class":57},[40,1407,1408,1411],{"class":42,"line":281},[40,1409,1410],{"class":57},"  --insight-type=google.iam.serviceAccount.Insight",[40,1412,58],{"class":57},[40,1414,1415,1418],{"class":42,"line":289},[40,1416,1417],{"class":57},"  --project=payments-prod",[40,1419,1420],{"class":57}," --location=global\n",[11,1422,1423,1424,1428],{},"The service account insight has blind spots Google documents: requests authenticated with API keys bound to a service account aren't counted, and neither is authentication to Google APIs outside Google Cloud, such as Workspace domain-wide delegation. Cross-check with Cloud Monitoring's service account usage metrics before you disable anything. Every remaining downloaded key is a flag for \"why does this still need to exist,\" not a fact of life; the ",[102,1425,1427],{"href":1426},"\u002F2026\u002F03\u002F04\u002Fnode-js-gcloud-rotating-service-account-keys-before-they-expire\u002F","key rotation script"," is what I use for the ones that really do.",[15,1430,1432],{"id":1431},"making-it-stick","Making it stick",[11,1434,1435],{},"None of this holds without the boring part: a recurring review cadence (the two CSVs above, every month, diffed against last month), an owner for every custom role, and enough cultural buy-in that a tightened permission gets reported as friction to investigate rather than quietly worked around with a personal access grant. The technical controls are the easy half. The discipline to keep re-applying them as the organization changes is the part that actually determines whether least privilege is real or just written down.",[15,1437,1439],{"id":1438},"references","References",[1441,1442,1443,1452,1459,1466,1473,1480,1487,1494,1501,1508],"ul",{},[1444,1445,1446],"li",{},[102,1447,1451],{"href":1448,"rel":1449},"https:\u002F\u002Fdocs.cloud.google.com\u002Fiam\u002Fdocs\u002Froles-overview",[1450],"nofollow","Roles and permissions overview",[1444,1453,1454],{},[102,1455,1458],{"href":1456,"rel":1457},"https:\u002F\u002Fdocs.cloud.google.com\u002Fpolicy-intelligence\u002Fdocs\u002Frole-recommendations-overview",[1450],"Role recommendations overview",[1444,1460,1461],{},[102,1462,1465],{"href":1463,"rel":1464},"https:\u002F\u002Fdocs.cloud.google.com\u002Fpolicy-intelligence\u002Fdocs\u002Freview-apply-role-recommendations",[1450],"Review and apply role recommendations",[1444,1467,1468],{},[102,1469,1472],{"href":1470,"rel":1471},"https:\u002F\u002Fdocs.cloud.google.com\u002Fpolicy-intelligence\u002Fdocs\u002Fservice-account-insights",[1450],"Find unused service accounts (service account insights)",[1444,1474,1475],{},[102,1476,1479],{"href":1477,"rel":1478},"https:\u002F\u002Fdocs.cloud.google.com\u002Fpolicy-intelligence\u002Fdocs\u002Factivity-analyzer-service-account-authentication",[1450],"Activity Analyzer: service account key authentication",[1444,1481,1482],{},[102,1483,1486],{"href":1484,"rel":1485},"https:\u002F\u002Fdocs.cloud.google.com\u002Fpolicy-intelligence\u002Fdocs\u002Fanalyze-iam-policies",[1450],"Analyze IAM policies (Policy Analyzer)",[1444,1488,1489],{},[102,1490,1493],{"href":1491,"rel":1492},"https:\u002F\u002Fdocs.cloud.google.com\u002Fasset-inventory\u002Fdocs\u002Fsearching-iam-policies",[1450],"Search IAM allow policies",[1444,1495,1496],{},[102,1497,1500],{"href":1498,"rel":1499},"https:\u002F\u002Fdocs.cloud.google.com\u002Fiam\u002Fdocs\u002Fcreating-custom-roles",[1450],"Create and manage custom roles",[1444,1502,1503],{},[102,1504,1507],{"href":1505,"rel":1506},"https:\u002F\u002Fdocs.cloud.google.com\u002Fiam\u002Fdocs\u002Fconfiguring-temporary-access",[1450],"Configure temporary access with IAM conditions",[1444,1509,1510],{},[102,1511,1514],{"href":1512,"rel":1513},"https:\u002F\u002Fdocs.cloud.google.com\u002Fiam\u002Fdocs\u002Fbest-practices-for-managing-service-account-keys",[1450],"Best practices for managing service account keys",[1516,1517,1518],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}",{"title":36,"searchDepth":61,"depth":61,"links":1520},[1521,1522,1523,1524,1525,1526,1527],{"id":17,"depth":61,"text":18},{"id":157,"depth":61,"text":158},{"id":364,"depth":61,"text":365},{"id":1272,"depth":61,"text":1273},{"id":1331,"depth":61,"text":1332},{"id":1431,"depth":61,"text":1432},{"id":1438,"depth":61,"text":1439},"techcolumnist",[1530],"engineering","2026-09-09T14:00:00Z","Running least privilege in a real Google Cloud org: finding basic roles, working the IAM recommender, custom roles, temporary access, and service account risk.","md",false,null,{},"\u002Fblog\u002F2026\u002F09\u002F09\u002Fgoogle-cloud-iam-least-privilege-in-practice-not-just-in-theory",{"title":6,"description":1532},[1528],"blog\u002F2026\u002F09\u002F09\u002Fgoogle-cloud-iam-least-privilege-in-practice-not-just-in-theory",[47],"\u002F2026\u002F09\u002F09\u002Fgoogle-cloud-iam-least-privilege-in-practice-not-just-in-theory\u002F","Ry_NINGe5e0MuoLAkd1RNt9AlhwXFpwwzyfc4iooQuo",{"title":1545,"description":1546,"date":1547,"url":1548,"categories":1549,"tags":1551,"image":1535,"readingTime":91,"canonical":1528,"sites":1555,"series":1535,"seriesOrder":1535},"Hyper-V to Talos: Rethinking the Virtualization Stack","Why moving workloads off Hyper-V VMs onto a Talos Kubernetes cluster changed the whole stack, with an inventory script and a VM-to-manifest example.","2026-09-16T14:00:00Z","\u002F2026\u002F09\u002F16\u002Fhyper-v-to-talos-rethinking-the-virtualization-stack\u002F",[1530,1550],"strategy",[1552,1553,1554],"hyper-v","talos","kubernetes",[1528],{"title":1557,"description":1558,"date":1559,"url":1560,"categories":1561,"tags":1562,"image":1535,"readingTime":91,"canonical":1528,"sites":1566,"series":1535,"seriesOrder":1535},"Windows Management: A Discovery Inventory Nobody Maintains by Hand","Why a self-refreshing Windows inventory built from AD and CIM discovery outlasts a spreadsheet, with the sweep script, staleness report and schedule.","2026-09-02T14:00:00Z","\u002F2026\u002F09\u002F02\u002Fwindows-management-building-a-discovery-inventory-nobody-has-to-maintain-by-hand\u002F",[1530,1550],[1563,1564,1565],"discovery","windows","active-directory",[1528],[1568,1575,1583],{"title":1569,"description":1570,"date":1571,"url":1345,"categories":1572,"tags":1573,"image":1535,"readingTime":273,"canonical":1528,"sites":1574,"series":1535,"seriesOrder":1535},"Google Cloud: Organization Policy Constraints That Actually Matter","Which Google Cloud organization policy constraints prevent real incidents, with policy YAML, the dry-run workflow, tag-based exemptions, and an audit script.","2026-05-13T14:00:00Z",[1530],[47],[1528],{"title":1576,"description":1577,"date":1578,"url":1579,"categories":1580,"tags":1581,"image":1535,"readingTime":273,"canonical":1528,"sites":1582,"series":1535,"seriesOrder":1535},"Google Cloud: Choosing Between GKE and Cloud Run for an Internal Tool","How to pick GKE Autopilot or Cloud Run for an internal tool: billing models, sidecars, IAP, VPC egress, and team fluency, with deploy scripts for both.","2026-05-06T14:00:00Z","\u002F2026\u002F05\u002F06\u002Fgoogle-cloud-choosing-between-gke-and-cloud-run-for-an-internal-tool\u002F",[1530],[47,1554],[1528],{"title":1584,"description":1585,"date":1586,"url":1426,"categories":1587,"tags":1589,"image":1535,"readingTime":80,"canonical":1528,"sites":1591,"series":1535,"seriesOrder":1535},"Node.js: gcloud – Rotating Service Account Keys Before They Expire","A Node.js script that finds aging Google Cloud service account keys, mints one replacement, and disables the old keys once it is deployed.","2026-03-04T14:00:00Z",[1588,1530],"scripts",[1590,47],"nodejs",[1528],{"doc":1535,"posts":1593},[],1790052514241]