[{"data":1,"prerenderedAt":945},["ShallowReactive",2],{"doc:\u002Fcloudflare-access":3},{"id":4,"title":5,"body":6,"description":938,"extension":939,"meta":940,"navigation":367,"path":941,"seo":942,"stem":943,"__hash__":944},"docs\u002FCLOUDFLARE-ACCESS.md","Cloudflare Access — \u002Fadmin\u002F* 보호 가이드",{"type":7,"value":8,"toc":922},"minimark",[9,19,29,40,43,48,113,115,119,145,147,151,156,279,283,286,309,311,315,323,629,632,718,721,723,727,734,741,771,774,776,783,802,804,808,857,859,863,874,876,879,890,892,895,918],[10,11,13,14,18],"h1",{"id":12},"cloudflare-access-admin-보호-가이드","Cloudflare Access — ",[15,16,17],"code",{},"\u002Fadmin\u002F*"," 보호 가이드",[20,21,22,24,25,28],"p",{},[15,23,17],{}," 라우트(현재: ",[15,26,27],{},"\u002Fadmin\u002Fcost",")와 함께 모든 노출 위험 페이지·API를 Cloudflare Access로 보호하는 단계별 가이드.",[30,31,32],"blockquote",{},[20,33,34,35,39],{},"현재 상태: ",[36,37,38],"strong",{},"무인증",". URL을 아는 사람은 누구나 비용·DB 탐색 엔드포인트에 접근 가능. 사내 운영 중에는 OK이지만, 공개 도메인에 둘 거라면 반드시 적용해야 한다.",[41,42],"hr",{},[44,45,47],"h2",{"id":46},"보호-대상","보호 대상",[49,50,51,67],"table",{},[52,53,54],"thead",{},[55,56,57,61,64],"tr",{},[58,59,60],"th",{},"자원",[58,62,63],{},"호스트",[58,65,66],{},"경로",[68,69,70,85,99],"tbody",{},[55,71,72,76,81],{},[73,74,75],"td",{},"LLM 비용 대시보드",[73,77,78],{},[15,79,80],{},"malgn-helper-pms.pages.dev",[73,82,83],{},[15,84,17],{},[55,86,87,90,95],{},[73,88,89],{},"비용 집계 API",[73,91,92],{},[15,93,94],{},"malgn-helper-api.malgnsoft.workers.dev",[73,96,97],{},[15,98,17],{},[55,100,101,104,108],{},[73,102,103],{},"DB 탐색 임시 엔드포인트",[73,105,106],{},[15,107,94],{},[73,109,110],{},[15,111,112],{},"\u002Fdb\u002F*",[41,114],{},[44,116,118],{"id":117},"사전-준비","사전 준비",[120,121,122,126],"ol",{},[123,124,125],"li",{},"Cloudflare Zero Trust 무료 플랜 활성화 (월 50명까지)",[123,127,128,129],{},"보호할 도메인이 Cloudflare에 등록되어 있어야 함\n",[130,131,132,142],"ul",{},[123,133,134,137,138,141],{},[15,135,136],{},"*.pages.dev"," 와 ",[15,139,140],{},"*.workers.dev","는 기본적으로 Access 적용 가능",[123,143,144],{},"커스텀 도메인이면 해당 zone 등록 필요",[41,146],{},[44,148,150],{"id":149},"_1-access-application-만들기","1) Access Application 만들기",[152,153,155],"h3",{"id":154},"pages-페이지-pms-보호","Pages 페이지 (PMS) 보호",[120,157,158,180,228,234],{},[123,159,160,166,167,170,171,170,174,170,177],{},[161,162,163],"a",{"href":163,"rel":164},"https:\u002F\u002Fone.dash.cloudflare.com",[165],"nofollow"," → 좌측 ",[36,168,169],{},"Access"," → ",[36,172,173],{},"Applications",[36,175,176],{},"Add an application",[36,178,179],{},"Self-hosted",[123,181,182,183],{},"입력:\n",[130,184,185,194,203],{},[123,186,187,190,191],{},[36,188,189],{},"Application name",": ",[15,192,193],{},"malgn-helper-pms admin",[123,195,196,190,199,202],{},[36,197,198],{},"Session Duration",[15,200,201],{},"24h"," (또는 원하는 값)",[123,204,205,208,209],{},[36,206,207],{},"Application domain",":\n",[130,210,211,217,223],{},[123,212,213,214],{},"Subdomain: ",[15,215,216],{},"malgn-helper-pms",[123,218,219,220],{},"Domain: ",[15,221,222],{},"pages.dev",[123,224,225,226],{},"Path: ",[15,227,17],{},[123,229,230,233],{},[36,231,232],{},"Identity providers",": One-time PIN (이메일 OTP) 또는 회사 SSO(Google Workspace\u002FOkta 등)",[123,235,236,208,239],{},[36,237,238],{},"Add a policy",[130,240,241,249,257,272],{},[123,242,243,190,246],{},[36,244,245],{},"Policy name",[15,247,248],{},"staff only",[123,250,251,190,254],{},[36,252,253],{},"Action",[15,255,256],{},"Allow",[123,258,259,208,262],{},[36,260,261],{},"Include",[130,263,264],{},[123,265,266,170,269],{},[15,267,268],{},"Emails ending in",[15,270,271],{},"@malgnsoft.com",[123,273,274,275,278],{},"또는: ",[15,276,277],{},"Emails"," → 명시적 화이트리스트",[152,280,282],{"id":281},"api-worker-보호","API Worker 보호",[20,284,285],{},"같은 절차로 두 번째 Application 추가:",[130,287,288,293,298,306],{},[123,289,213,290],{},[15,291,292],{},"malgn-helper-api",[123,294,219,295],{},[15,296,297],{},"workers.dev",[123,299,225,300,302,303,305],{},[15,301,17],{}," (또는 ",[15,304,112],{},"까지 보호하려면 추가 Application)",[123,307,308],{},"동일 policy 적용",[41,310],{},[44,312,314],{"id":313},"_2-worker가-access-jwt를-검증하도록-옵션-강력-권장","2) Worker가 Access JWT를 검증하도록 (옵션, 강력 권장)",[20,316,317,318,322],{},"Access를 켜도 ",[319,320,321],"em",{},"직접 Worker 도메인 IP에 도달하는 우회 요청","은 막을 수 없다. Worker 측에서 한 번 더 검증해야 완전:",[324,325,330],"pre",{"className":326,"code":327,"language":328,"meta":329,"style":329},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F src\u002Fmiddleware\u002Faccess.ts\nimport { jwtVerify, createRemoteJWKSet } from \"jose\";\n\nconst JWKS = createRemoteJWKSet(\n  new URL(\"https:\u002F\u002F\u003Cteam>.cloudflareaccess.com\u002Fcdn-cgi\u002Faccess\u002Fcerts\"),\n);\nconst AUD = \"\u003Capplication-aud-tag>\"; \u002F\u002F Access Application의 AUD\n\nexport async function requireAccess(c: any) {\n  const token = c.req.header(\"cf-access-jwt-assertion\");\n  if (!token) return c.json({ error: \"no-access-jwt\" }, 401);\n  try {\n    await jwtVerify(token, JWKS, {\n      issuer: \"https:\u002F\u002F\u003Cteam>.cloudflareaccess.com\",\n      audience: AUD,\n    });\n  } catch {\n    return c.json({ error: \"invalid-access-jwt\" }, 401);\n  }\n}\n","ts","",[15,331,332,341,362,369,389,407,413,432,437,467,491,529,538,556,568,579,585,596,617,623],{"__ignoreMap":329},[333,334,337],"span",{"class":335,"line":336},"line",1,[333,338,340],{"class":339},"sJ8bj","\u002F\u002F src\u002Fmiddleware\u002Faccess.ts\n",[333,342,344,348,352,355,359],{"class":335,"line":343},2,[333,345,347],{"class":346},"szBVR","import",[333,349,351],{"class":350},"sVt8B"," { jwtVerify, createRemoteJWKSet } ",[333,353,354],{"class":346},"from",[333,356,358],{"class":357},"sZZnC"," \"jose\"",[333,360,361],{"class":350},";\n",[333,363,365],{"class":335,"line":364},3,[333,366,368],{"emptyLinePlaceholder":367},true,"\n",[333,370,372,375,379,382,386],{"class":335,"line":371},4,[333,373,374],{"class":346},"const",[333,376,378],{"class":377},"sj4cs"," JWKS",[333,380,381],{"class":346}," =",[333,383,385],{"class":384},"sScJk"," createRemoteJWKSet",[333,387,388],{"class":350},"(\n",[333,390,392,395,398,401,404],{"class":335,"line":391},5,[333,393,394],{"class":346},"  new",[333,396,397],{"class":384}," URL",[333,399,400],{"class":350},"(",[333,402,403],{"class":357},"\"https:\u002F\u002F\u003Cteam>.cloudflareaccess.com\u002Fcdn-cgi\u002Faccess\u002Fcerts\"",[333,405,406],{"class":350},"),\n",[333,408,410],{"class":335,"line":409},6,[333,411,412],{"class":350},");\n",[333,414,416,418,421,423,426,429],{"class":335,"line":415},7,[333,417,374],{"class":346},[333,419,420],{"class":377}," AUD",[333,422,381],{"class":346},[333,424,425],{"class":357}," \"\u003Capplication-aud-tag>\"",[333,427,428],{"class":350},"; ",[333,430,431],{"class":339},"\u002F\u002F Access Application의 AUD\n",[333,433,435],{"class":335,"line":434},8,[333,436,368],{"emptyLinePlaceholder":367},[333,438,440,443,446,449,452,454,458,461,464],{"class":335,"line":439},9,[333,441,442],{"class":346},"export",[333,444,445],{"class":346}," async",[333,447,448],{"class":346}," function",[333,450,451],{"class":384}," requireAccess",[333,453,400],{"class":350},[333,455,457],{"class":456},"s4XuR","c",[333,459,460],{"class":346},":",[333,462,463],{"class":377}," any",[333,465,466],{"class":350},") {\n",[333,468,470,473,476,478,481,484,486,489],{"class":335,"line":469},10,[333,471,472],{"class":346},"  const",[333,474,475],{"class":377}," token",[333,477,381],{"class":346},[333,479,480],{"class":350}," c.req.",[333,482,483],{"class":384},"header",[333,485,400],{"class":350},[333,487,488],{"class":357},"\"cf-access-jwt-assertion\"",[333,490,412],{"class":350},[333,492,494,497,500,503,506,509,512,515,518,521,524,527],{"class":335,"line":493},11,[333,495,496],{"class":346},"  if",[333,498,499],{"class":350}," (",[333,501,502],{"class":346},"!",[333,504,505],{"class":350},"token) ",[333,507,508],{"class":346},"return",[333,510,511],{"class":350}," c.",[333,513,514],{"class":384},"json",[333,516,517],{"class":350},"({ error: ",[333,519,520],{"class":357},"\"no-access-jwt\"",[333,522,523],{"class":350}," }, ",[333,525,526],{"class":377},"401",[333,528,412],{"class":350},[333,530,532,535],{"class":335,"line":531},12,[333,533,534],{"class":346},"  try",[333,536,537],{"class":350}," {\n",[333,539,541,544,547,550,553],{"class":335,"line":540},13,[333,542,543],{"class":346},"    await",[333,545,546],{"class":384}," jwtVerify",[333,548,549],{"class":350},"(token, ",[333,551,552],{"class":377},"JWKS",[333,554,555],{"class":350},", {\n",[333,557,559,562,565],{"class":335,"line":558},14,[333,560,561],{"class":350},"      issuer: ",[333,563,564],{"class":357},"\"https:\u002F\u002F\u003Cteam>.cloudflareaccess.com\"",[333,566,567],{"class":350},",\n",[333,569,571,574,577],{"class":335,"line":570},15,[333,572,573],{"class":350},"      audience: ",[333,575,576],{"class":377},"AUD",[333,578,567],{"class":350},[333,580,582],{"class":335,"line":581},16,[333,583,584],{"class":350},"    });\n",[333,586,588,591,594],{"class":335,"line":587},17,[333,589,590],{"class":350},"  } ",[333,592,593],{"class":346},"catch",[333,595,537],{"class":350},[333,597,599,602,604,606,608,611,613,615],{"class":335,"line":598},18,[333,600,601],{"class":346},"    return",[333,603,511],{"class":350},[333,605,514],{"class":384},[333,607,517],{"class":350},[333,609,610],{"class":357},"\"invalid-access-jwt\"",[333,612,523],{"class":350},[333,614,526],{"class":377},[333,616,412],{"class":350},[333,618,620],{"class":335,"line":619},19,[333,621,622],{"class":350},"  }\n",[333,624,626],{"class":335,"line":625},20,[333,627,628],{"class":350},"}\n",[20,630,631],{},"라우트에 적용:",[324,633,635],{"className":326,"code":634,"language":328,"meta":329,"style":329},"app.use(\"\u002Fadmin\u002F*\", async (c, next) => {\n  const fail = await requireAccess(c);\n  if (fail) return fail;\n  await next();\n});\n",[15,636,637,673,690,702,713],{"__ignoreMap":329},[333,638,639,642,645,647,650,653,656,658,660,662,665,668,671],{"class":335,"line":336},[333,640,641],{"class":350},"app.",[333,643,644],{"class":384},"use",[333,646,400],{"class":350},[333,648,649],{"class":357},"\"\u002Fadmin\u002F*\"",[333,651,652],{"class":350},", ",[333,654,655],{"class":346},"async",[333,657,499],{"class":350},[333,659,457],{"class":456},[333,661,652],{"class":350},[333,663,664],{"class":456},"next",[333,666,667],{"class":350},") ",[333,669,670],{"class":346},"=>",[333,672,537],{"class":350},[333,674,675,677,680,682,685,687],{"class":335,"line":343},[333,676,472],{"class":346},[333,678,679],{"class":377}," fail",[333,681,381],{"class":346},[333,683,684],{"class":346}," await",[333,686,451],{"class":384},[333,688,689],{"class":350},"(c);\n",[333,691,692,694,697,699],{"class":335,"line":364},[333,693,496],{"class":346},[333,695,696],{"class":350}," (fail) ",[333,698,508],{"class":346},[333,700,701],{"class":350}," fail;\n",[333,703,704,707,710],{"class":335,"line":371},[333,705,706],{"class":346},"  await",[333,708,709],{"class":384}," next",[333,711,712],{"class":350},"();\n",[333,714,715],{"class":335,"line":391},[333,716,717],{"class":350},"});\n",[20,719,720],{},"AUD는 Access Application 상세에서 확인 (Audience tag).",[41,722],{},[44,724,726],{"id":725},"_3-pms에서-페이지가-access-토큰을-자동으로-처리하도록","3) PMS에서 페이지가 Access 토큰을 자동으로 처리하도록",[20,728,729,730,733],{},"Pages 도메인 자체에 Access를 걸면 브라우저가 자동 로그인 → cookie\u002Fheader 자동 첨부. ",[36,731,732],{},"추가 코드 불필요",".",[20,735,736,737,740],{},"PMS → API 호출 시 cross-origin이면 ",[15,738,739],{},"credentials: \"include\""," 필요할 수 있음:",[324,742,744],{"className":326,"code":743,"language":328,"meta":329,"style":329},"fetch(`${API_BASE}\u002Fadmin\u002Fcost`, { credentials: \"include\" })\n",[15,745,746],{"__ignoreMap":329},[333,747,748,751,753,756,759,762,765,768],{"class":335,"line":336},[333,749,750],{"class":384},"fetch",[333,752,400],{"class":350},[333,754,755],{"class":357},"`${",[333,757,758],{"class":377},"API_BASE",[333,760,761],{"class":357},"}\u002Fadmin\u002Fcost`",[333,763,764],{"class":350},", { credentials: ",[333,766,767],{"class":357},"\"include\"",[333,769,770],{"class":350}," })\n",[20,772,773],{},"만약 API가 다른 zone이면 별도 Application 통과 — 사용자가 한 번 더 OTP 로그인.",[41,775],{},[44,777,779,780,782],{"id":778},"_4-db-탐색-엔드포인트-보호-db","4) DB 탐색 엔드포인트 보호 (",[15,781,112],{},")",[20,784,785,652,788,652,791,652,794,797,798,801],{},[15,786,787],{},"\u002Fdb\u002Fwhoami",[15,789,790],{},"\u002Fdb\u002Ftables",[15,792,793],{},"\u002Fdb\u002Fcolumns\u002F:t",[15,795,796],{},"\u002Fdb\u002Fsample\u002F:t"," 모두 운영 전 단계 노출. Access Application 추가하거나, ",[36,799,800],{},"차라리 제거"," 후 필요 시 일회용 admin 엔드포인트로 다시 만드는 게 깔끔.",[41,803],{},[44,805,807],{"id":806},"_5-적용-후-검증","5) 적용 후 검증",[324,809,813],{"className":810,"code":811,"language":812,"meta":329,"style":329},"language-bash shiki shiki-themes github-light github-dark","# Access 없이 호출 → 302 (Access 로그인 페이지로 리다이렉트) 또는 401\ncurl -sS -o \u002Fdev\u002Fnull -w \"%{http_code}\\n\" https:\u002F\u002Fmalgn-helper-api.malgnsoft.workers.dev\u002Fadmin\u002Fcost\n# → 302 또는 401 기대\n\n# 브라우저에서 페이지 접속 → OTP 메일 → 로그인 → 정상 표시\n","bash",[15,814,815,820,843,848,852],{"__ignoreMap":329},[333,816,817],{"class":335,"line":336},[333,818,819],{"class":339},"# Access 없이 호출 → 302 (Access 로그인 페이지로 리다이렉트) 또는 401\n",[333,821,822,825,828,831,834,837,840],{"class":335,"line":343},[333,823,824],{"class":384},"curl",[333,826,827],{"class":377}," -sS",[333,829,830],{"class":377}," -o",[333,832,833],{"class":357}," \u002Fdev\u002Fnull",[333,835,836],{"class":377}," -w",[333,838,839],{"class":357}," \"%{http_code}\\n\"",[333,841,842],{"class":357}," https:\u002F\u002Fmalgn-helper-api.malgnsoft.workers.dev\u002Fadmin\u002Fcost\n",[333,844,845],{"class":335,"line":364},[333,846,847],{"class":339},"# → 302 또는 401 기대\n",[333,849,850],{"class":335,"line":371},[333,851,368],{"emptyLinePlaceholder":367},[333,853,854],{"class":335,"line":391},[333,855,856],{"class":339},"# 브라우저에서 페이지 접속 → OTP 메일 → 로그인 → 정상 표시\n",[41,858],{},[44,860,862],{"id":861},"_6-비상시-해제","6) 비상시 해제",[130,864,865,871],{},[123,866,867,868],{},"Dashboard → Access → Applications → 해당 Application → ",[36,869,870],{},"Delete",[123,872,873],{},"Worker 측 미들웨어는 그대로 두면 401만 반환 → 미들웨어 임시 주석 처리 후 재배포",[41,875],{},[44,877,878],{"id":878},"비용",[130,880,881,887],{},[123,882,883,884],{},"Zero Trust Free: ",[36,885,886],{},"50명까지 무료",[123,888,889],{},"그 이상: 사용자당 월 $3 (Standard) — 현재 운영팀 규모면 무료 한도 안에 충분",[41,891],{},[44,893,894],{"id":894},"참고",[130,896,897,904,911],{},[123,898,899,900],{},"공식 가이드: ",[161,901,902],{"href":902,"rel":903},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fapplications\u002Fconfigure-apps\u002Fself-hosted-apps\u002F",[165],[123,905,906,907],{},"Worker에서 JWT 검증: ",[161,908,909],{"href":909,"rel":910},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-one\u002Fidentity\u002Fauthorization-cookie\u002Fvalidating-json\u002F",[165],[123,912,913,914,917],{},"AUD 위치: Application 상세 → ",[36,915,916],{},"Overview"," 탭 → \"Application Audience (AUD) Tag\"",[919,920,921],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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);}",{"title":329,"searchDepth":364,"depth":364,"links":923},[924,925,926,930,931,932,934,935,936,937],{"id":46,"depth":343,"text":47},{"id":117,"depth":343,"text":118},{"id":149,"depth":343,"text":150,"children":927},[928,929],{"id":154,"depth":364,"text":155},{"id":281,"depth":364,"text":282},{"id":313,"depth":343,"text":314},{"id":725,"depth":343,"text":726},{"id":778,"depth":343,"text":933},"4) DB 탐색 엔드포인트 보호 (\u002Fdb\u002F*)",{"id":806,"depth":343,"text":807},{"id":861,"depth":343,"text":862},{"id":878,"depth":343,"text":878},{"id":894,"depth":343,"text":894},"\u002Fadmin\u002F* 라우트(현재: \u002Fadmin\u002Fcost)와 함께 모든 노출 위험 페이지·API를 Cloudflare Access로 보호하는 단계별 가이드.","md",{},"\u002Fcloudflare-access",{"title":5,"description":938},"CLOUDFLARE-ACCESS","7ITx0fVBm2H7cGdMsM7FCT0HZNIPfkwycHIwQfQ08QY",1780986551103]