이 글이 답하는 질문
SAP BTP에 Node.js나 Java 백엔드를 배포할 때 Approuter를 빼먹으면 정확히 무엇이 위험한가? xs-app.json의 routes 배열은 어떤 순서로 매칭되며, source 정규식에서 ^를 빠뜨리면 왜 보안 사고로 이어지는가? destination, authenticationType, csrfProtection 같은 필드는 각각 언제 어떻게 써야 하는가? 이 글을 다 읽으면 다음 체크리스트를 스스로 점검할 수 있습니다.
- Approuter가 XSUAA 토큰을 어떻게 검증하고 백엔드에 전달하는지 설명할 수 있다
- xs-app.json의 routes를 안전하게 작성하고 매칭 순서를 통제할 수 있다
- destination 기반 라우팅과 localDir 정적 서빙을 구분해서 쓸 수 있다
- csrfProtection 토글이 필요한 시점을 판단할 수 있다
이 글을 보기 전에
Cloud Foundry 또는 Kyma 환경에서 cf push 또는 mta 빌드 한 번 정도는 돌려본 경험을 가정합니다. XSUAA 서비스 인스턴스가 무엇인지, JWT 토큰의 대략적인 구조(헤더/페이로드/서명)는 알고 있어야 합니다.
사용한 버전
- SAP BTP Cloud Foundry runtime (2026년 기준)
@sap/approuter14.x 계열- Node.js 20 LTS
- MTA Build Tool (mbt) 1.2.x
- Cloud Foundry CLI 8.x
- XSUAA 서비스 플랜 application
Approuter가 없으면 어떤 일이 생기나
Approuter를 건너뛰고 백엔드 모듈에 직접 라우트 매핑을 걸어두면 세 가지 문제가 동시에 발생합니다.
- 인증 미검증 노출: XSUAA OAuth2 흐름은 Approuter가 브라우저와 IdP 사이에서 authorization_code grant를 중계하면서 시작됩니다. Approuter가 없으면 백엔드 컨테이너가 외부 URL을 직접 받고, 토큰 검증 미들웨어를 모듈마다 따로 설치해야 합니다. 빠뜨리는 순간 무인증 API가 됩니다.
- 세션/쿠키 일관성 깨짐: Approuter는 JSESSIONID와 XSRF 토큰을 한 도메인 안에서 관리합니다. 마이크로서비스가 두세 개로 늘어나면 도메인별 쿠키 정책 충돌로 로그인 루프가 발생합니다.
- CORS와 CSRF 우회: 백엔드를 직접 노출하면 SameSite 정책이 깨지고, 공격자가 XSUAA 발급 토큰을 가로채 다른 오리진에서 호출할 여지가 생깁니다.
Approuter는 단순한 리버스 프록시가 아니라 BTP 보안 모델의 정문입니다. 정문을 없애면 집안 모든 방문에 자물쇠를 따로 달아야 합니다.
Approuter 동작 원리 (XSUAA 토큰 검증 흐름)
- 브라우저가
https://myapp.cfapps.eu10.hana.ondemand.com/으로 진입 - Approuter는 세션 쿠키 확인. 없으면 XSUAA로 302 리다이렉트, authorization_code 흐름 시작
- 사용자가 IdP에 로그인하면 XSUAA가 JWT access_token + refresh_token 발급
- Approuter가 토큰을 서버 세션에 저장하고, 이후 모든 백엔드 호출에
Authorization: Bearer ...헤더 자동 첨부 - 백엔드는
@sap/xssec로 토큰 서명/만료/스코프 검증
xs-app.json 기본 구조와 필수 필드
xs-app.json은 Approuter의 라우팅 테이블입니다. 최상위 키부터 정리합니다.
welcomeFile: 루트 진입 시 기본 파일 (예:/index.html)authenticationMethod: 전역 인증 방식, 보통routesessionTimeout: 분 단위 세션 만료routes: 라우트 객체 배열, 위에서 아래로 첫 매칭 적용
routes 객체의 핵심 필드: source(정규식), target, destination, localDir, authenticationType, csrfProtection. source는 ^와 $를 명시적으로 써야 부분 매칭 사고를 막을 수 있습니다.
직접 해보기
1단계: 기본 정적 라우트와 보호된 진입점
{
"welcomeFile": "/index.html",
"authenticationMethod": "route",
"sessionTimeout": 30,
"routes": [
{
"source": "^/app/(.*)$",
"target": "$1",
"localDir": "webapp",
"authenticationType": "xsuaa",
"cacheControl": "no-cache, no-store, must-revalidate"
}
]
}
^/app/(.*)$ 패턴은 정확히 /app/으로 시작하는 경로만 매칭합니다. $1은 캡처 그룹 첫 번째 값으로 치환되어 webapp/ 하위 파일을 찾아갑니다.
2단계: 백엔드 API 프록시와 destination 연결
{
"welcomeFile": "/app/index.html",
"authenticationMethod": "route",
"routes": [
{
"source": "^/odata/v4/(.*)$",
"target": "/odata/v4/$1",
"destination": "srv-api",
"authenticationType": "xsuaa",
"csrfProtection": true,
"scope": {
"GET": "$XSAPPNAME.Viewer",
"default": "$XSAPPNAME.Editor"
}
},
{
"source": "^/app/(.*)$",
"target": "$1",
"localDir": "webapp",
"authenticationType": "xsuaa"
}
]
}
mta.yaml에서 destination 이름을 맞춰줍니다.
modules:
- name: myapp-approuter
type: approuter.nodejs
path: app-router
requires:
- name: myapp-srv-api
group: destinations
properties:
name: srv-api
url: ~{srv-url}
forwardAuthToken: true
- name: myapp-uaa
3단계: csrfProtection 토글과 헤더 화이트리스트
외부 시스템에서 머신 투 머신으로 호출하는 webhook 엔드포인트는 CSRF 토큰을 요구하면 안 됩니다.
{
"welcomeFile": "/app/index.html",
"authenticationMethod": "route",
"logout": {
"logoutEndpoint": "/do/logout",
"logoutPage": "/app/logout.html"
},
"routes": [
{
"source": "^/webhook/(.*)$",
"target": "/webhook/$1",
"destination": "srv-api",
"authenticationType": "xsuaa",
"csrfProtection": false,
"scope": "$XSAPPNAME.WebhookCaller"
},
{
"source": "^/odata/v4/(.*)$",
"target": "/odata/v4/$1",
"destination": "srv-api",
"authenticationType": "xsuaa",
"csrfProtection": true
},
{
"source": "^/health$",
"target": "/health",
"destination": "srv-api",
"authenticationType": "none"
},
{
"source": "^/app/(.*)$",
"target": "$1",
"localDir": "webapp",
"authenticationType": "xsuaa"
}
],
"headerWhiteList": [
"x-correlation-id",
"x-request-id",
"traceparent"
]
}
health 체크 엔드포인트는 authenticationType: none으로 두어 BTP 플랫폼 모니터링이 토큰 없이 접근하도록 허용합니다.
삽질 노트
- API가 모두 404: 와일드카드 라우트
^/(.*)$를 위에 두면 모든 요청을 흡수합니다. 구체적인 패턴을 위에, 넓은 패턴을 아래에 배치하세요. - 의도치 않은 경로 매칭: source를
/api/(.*)처럼 앵커 없이 쓰면 문자열 어디에든/api/가 포함되면 매칭됩니다.^/api/(.*)$로 양 끝을 고정하는 것을 습관화하세요. 보안 감사에서 가장 자주 지적되는 항목입니다. - 502 Bad Gateway: destination 이름이 mta.yaml의
properties.name, xs-app.json의destination값, BTP Destination Service 등록 이름 세 군데가 모두 일치해야 합니다. - 백엔드에 토큰이 도착 안 함: mta.yaml의 destination에
forwardAuthToken: true가 누락된 경우입니다. - 403 Forbidden: xs-security.json의 scope 이름 오타 또는 Role Collection에 스코프가 매핑되지 않은 경우.
cf logs <approuter-app> --recent를 먼저 확인하세요.
더 파볼 주제
- Central Approuter vs Managed Approuter — SAP Build Work Zone 환경 패턴
- Identity Authentication Service(IAS) 연동 — XSUAA 트러스트 설정과 SAML 어설션 매핑
- Role Collection 자동 매핑 — xs-security.json role-templates 구성
- WebSocket 라우팅 — ws/wss 업그레이드 처리와 sticky session
- Kyma 환경의 APIRule — Approuter를 Istio Gateway와 함께 쓰는 패턴
핵심 한 줄
Approuter는 BTP 보안 모델의 정문이고, xs-app.json은 그 문의 도면입니다. ^와 destination 이름 하나 빠뜨리면 정문이 활짝 열립니다.
댓글 0
아직 댓글이 없습니다.