F5 WAF for NGINX – JWT Validation
이 포스트에서는 F5 WAF for NGINX 를 활용하여 JSON Web Token (JWT) 보호 기능을 통해 인증 및 권한 부여를 강화하고, 무효한 토큰을 탐지·차단하는 방법을 소개합니다.
현대 API 기반 애플리케이션에서 JWT는 사용자 인증과 권한 관리를 위한 필수 요소입니다. 그러나 토큰 위조, 만료, 구조 오류 등으로 인한 보안 취약점이 발생할 수 있습니다. F5 WAF for NGINX의 JWT Protection은 토큰의 존재, 구조, 서명, 유효 기간을 검증하며, 이를 통해 애플리케이션 서버 부하를 줄이고 보안을 강화합니다. 이 기능은 RSA, PSS, ECDSA 등 다양한 알고리즘을 지원하며, accessProfile과 authorizationRules를 통해 세밀한 제어를 제공합니다.
목차
1. F5 WAF for NGINX – JWT Protection이란?
1-1. JWT 공격의 개요
2. WAF for NGINX 구성
2-1. 정책 핵심 설정 상세 설명
2-2. 정책 확장 및 고급 설정
3. 결론
1. F5 WAF for NGINX – JWT Protection이란?
JSON Web Token (JWT)은 두 당사자 간 정보를 JSON 형식으로 안전하게 전달하는 컴팩트한 방법으로, 주로 API 인증·권한 부여에 사용됩니다. F5 WAF for NGINX는 JWT의 유효성을 검증하여 무효 토큰 접근을 차단합니다.
JWT는 Header(메타데이터), Claims(주장 정보), Signature(서명) 세 부분으로 구성되며, Base64 인코딩된 형태로 “.”으로 구분됩니다. 일반적으로 Authorization 헤더의 Bearer 타입으로 전달되지만, 쿼리 파라미터로도 가능합니다. F5 WAF는 토큰 존재, 구조, 서명 검증, 유효 기간(nbf, exp), 사용자 추출 등을 처리합니다. 지원 알고리즘: RSA(RS256 등), PSS(PS256 등), ECDSA(ES256 등), EdDSA.
1-1. JWT 공격의 개요
JWT 공격은 일반적으로 다음과 같은 방식으로 이루어집니다:
- 토큰 위조: 서명 알고리즘 변경(None 알고리즘)이나 약한 키 사용.
- 만료 토큰 재사용: exp/nbf 무시로 만료된 토큰 악용.
- 구조 오류: Base64 디코딩 실패나 JSON 파싱 오류 유발.
- 권한 상승: Claims 조작으로 권한 없는 접근.
F5 WAF는 VIOL_ACCESS_INVALID 등 위반으로 이러한 패턴을 탐지하며, accessProfile과 authorizationRules로 방어합니다.
2. F5 WAF for NGINX 구성
아래는 JWT Protection을 위한 WAF 정책 구성 예시입니다. accessProfile로 토큰 검증을 정의하고, 특정 URL(/products/)에 적용합니다.
{
"policy": {
"name": "jwt_policy",
"template": {
"name": "POLICY_TEMPLATE_NGINX_BASE"
},
"applicationLanguage": "utf-8",
"enforcementMode": "blocking",
"bot-defense": {
"settings": {
"isEnabled": false
}
},
"blocking-settings": {
"violations": [
{
"alarm": true,
"block": true,
"name": "VIOL_ACCESS_INVALID"
},
{
"alarm": true,
"block": true,
"name": "VIOL_ACCESS_MISSING"
},
{
"alarm": true,
"block": true,
"name": "VIOL_ACCESS_MALFORMED"
},
{
"alarm": true,
"block": true,
"name": "VIOL_ACCESS_UNAUTHORIZED"
}
]
},
"access-profiles": [
{
"description": "",
"enforceMaximumLength": true,
"enforceValidityPeriod": true,
"keyFiles": [
{
"contents": "{\r\n \"keys\": [\r\n {\r\n \"alg\": \"RS256\",\r\n \"e\": \"AQAB\",\r\n \"kty\": \"RSA\",\r\n \"n\": \"0TU2NaNyiL6d_lIM20JZ2jeakX3rin3475pdbnvDxxmARUwxPcK2y_WAIlri1QEcyjXyhE2jJjQ88gbdDZmIvquQlI8ob_Ih6xSG_VPRiZoprQS8rdGVth6-yxuSbYP_ba9omP-PX_lgqFKXuAuhKTyqI8ynR-K_uQhHpkhclAjrrFlfnL4O1edfgtmSqUrCfnE3l9DVlyu_mSvQaeRAiwdkEAELlBeRqMTpIqFNWY9eUav5zAj-WzJZQ5uT1ZntMAK6cBjAWYGlKrqJQx-XtK_DXfOUwH9Fo13dVxBFPu5a0E0Q30c568ktQjcqSv1KG9V8dptye_iGIKMxGCAHpw\",\r\n \"kid\": \"nginxstore\",\r\n \"use\": \"sig\",\r\n \"x5c\": [\r\n \"MIICqDCCAZCgAwIBAgIGAZpxJe9DMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNVBAMMCm5naW54c3RvcmUwHhcNMjUxMTExMDQyMTQyWhcNMjYwOTA3MDQyMTQyWjAVMRMwEQYDVQQDDApuZ2lueHN0b3JlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0TU2NaNyiL6d/lIM20JZ2jeakX3rin3475pdbnvDxxmARUwxPcK2y/WAIlri1QEcyjXyhE2jJjQ88gbdDZmIvquQlI8ob/Ih6xSG/VPRiZoprQS8rdGVth6+yxuSbYP/ba9omP+PX/lgqFKXuAuhKTyqI8ynR+K/uQhHpkhclAjrrFlfnL4O1edfgtmSqUrCfnE3l9DVlyu/mSvQaeRAiwdkEAELlBeRqMTpIqFNWY9eUav5zAj+WzJZQ5uT1ZntMAK6cBjAWYGlKrqJQx+XtK/DXfOUwH9Fo13dVxBFPu5a0E0Q30c568ktQjcqSv1KG9V8dptye/iGIKMxGCAHpwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCDoq6zJ2qtyxipXZNVMPxmk9FQkzLsvF94n7x+wpxO9G2hWcwFm9Ua38y7nhuqAEtSMadIkbAjOEJ5ZgRDmK8WPri26leFWVNxS9qsPcHnJawO6SaR7ycLrAGNp+SzbW8e63c2iDkjbyH+TsgppsGQXyIeWBbRccWDZLZVnVilsG7ofY3+LXmTfpbSZPzngO2GqJeln+XYGkZkmJeZxvM88oimXIkGcgAiB+vyXvvvgRrz0W4LVURct9e6Z0RY5vbyG93uY66AOwi8+aWK+uM6M4PaFVshZgohwX1lNhYdfQaUZvLeW6LLyyNGcYslOS30VRNfURytin6EtdFDwx6e\"\r\n ]\r\n }\r\n ]\r\n}",
"fileName": "JWKSFile.json"
}
],
"location": {
"in": "header",
"name": "authorization"
},
"maximumLength": 2000,
"name": "access_profile_jwt",
"type": "jwt",
"usernameExtraction": {
"claimPropertyName": "NGINXSTORE",
"enabled": true,
"isMandatory": true
},
"verifyDigitalSignature": true
}
],
"urls": [
{
"name": "/products/",
"accessProfile": {
"name": "access_profile_jwt"
},
"authorizationRules": [
{
"condition": "claims['user'].contains('devopsshin')",
"name": "users"
}
],
"attackSignaturesCheck": true,
"mandatoryBody": false,
"method": "*",
"name": "/products/",
"protocol": "https",
"type": "explicit"
}
]
}
}
위 정책을 적용한 후 nginx -s reload 명령을 실행하여 설정을 반영합니다.
2-1. 정책 핵심 설정 상세 설명
access-profiles | JWT 검증 프로필. type: "jwt"로 정의. |
enforceMaximumLength: true | maximumLength 초과 시 차단. |
enforceValidityPeriod: true | nbf/exp 유효 기간 검사 활성화. |
keyFiles | JWKS 파일로 서명 검증 키 제공. |
location | 토큰 위치: 헤더(Authorization). |
usernameExtraction | Claims 추출 (claimPropertyName: "NGINXSTORE"). |
verifyDigitalSignature: true | 서명 검증 활성화. |
2-2. 정책 확장 및 고급 설정
JWT Protection은 authorizationRules로 Claims 기반 권한 검사를 확장할 수 있습니다:
- authorizationRules: 조건식으로 접근 제어(예: claims[‘user’].contains(‘devopsshin’)).
- blocking-settings: VIOL_ACCESS_INVALID 등 위반 차단/알람 설정.
- urls: accessProfile 연동 URL 지정(/products/).
이 설정으로 Claims 조작 공격 방지. 문서에 따라 조건식은 AND/OR, contains 등 지원.
1. user 값이 devopsshin이 아닌 경우:

위의 토큰을 사용하여 요청 시:
$ curl -k -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im5naW54c3RvcmUifQ.eyJzdWIiOiJOR0lOWCIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjIsIk5HSU5YU1RPUkUiOiJWYWxpZCIsInVzZXIiOiJkZXZvcHNzc3MiLCJleHAiOjE5OTk5OTk5OTl9.W95ymEnXiExNBj-LM-zqvTj9PTO0CgjK2Tn7LWoPb1qBqadKTTCt_z1YGasGN1rnWkw_2e5Dv58gFylkEMSx_P81ZPjj5zLhSzA45783gNLQbsTZomJh_o-KQIShQTV7AlkixYF9xExafb1IJW7OTnrIRlrOrPjwaEX1zO0qYe150rIYcTKEgyRGZTfc14eZWU_EBoV4AMwZzWcAHGrt1eH9e473l8L7JA33_d9XZkRz4EldOiVW76sejg6_yBiHDzPltLJrK7x7eAmxsZxPizKq_rNRGWrH2TC217wQX6BclKOhMWLZ4sMriHdblyZdA1bRMwmzxCjs9_2A1otH7A" https://ho.devops.com/products/
<html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.<br><br>Your support ID is: 11199271168854487288<br><br><a href='javascript:history.back();'>[Go Back]</a></body></html>
2. claim 값에 devopsshin은 포함되었지만 토큰의 만료 기간이 지났을 경우:

위의 토큰을 사용하여 요청 시:
$ curl -k -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im5naW54c3RvcmUifQ.eyJzdWIiOiJOR0lOWCIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjIsIk5HSU5YU1RPUkUiOiJWYWxpZCIsInVzZXIiOiJkZXZvcHNzaGluIiwiZXhwIjoxMDAwMDk5OTk5fQ.dNhZ3UYigmVSxEn7qcsZAZnNiYTSJOKJYbS1n8LPJolAokkxSw1rZPu-UBxbOfMqcT6Iyw6mnbtAYS8J992U2Im9tf5Y1fq4VfY-88_0oxq_oTRj50omFztGGYj0OrmzX8nnZfJu8p7Y_bdiAEjlcUDs7jtA2kHKlIyv5lpjYNxPr0GhS6IW8MBcNwVZgY50e310wUTEoqweny2btFyjAuODu8PlXb_YGJKLbv4QWxFeuv70F0WbOAecxMXxmreZnkseFffGo9IDuWmQZhNC0dAfTSpAcuwJ_4mAm8aiFtbDl_SP1MkADJ6H_vk9z_Ac-VeI_pXITtRLScV9_6TZxw" https://ho.devops.com/products/
<html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.<br><br>Your support ID is: 12470348570034638733<br><br><a href='javascript:history.back();'>[Go Back]</a></body></html>
3. NGINXSTORE라는 claim이 없는 경우:

위의 토큰을 사용하여 요청 시:
$ curl -k -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im5naW54c3RvcmUifQ.eyJzdWIiOiJOR0lOWCIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjIsInVzZXIiOiJkZXZvcHNzaGluIiwiZXhwIjoxOTk5OTk5OTk5fQ.AjSAGfr5RawXI71a9-aW7dFaSvoUoJQNLYNGBH2xAlv2DNYK8LayxR5nIvKaPJ546gYKJXKUSKL-o83CRMTEjxj9cybhw7EIfpJXjttNnzlRZvoxxcZbkR-R6aoJBKyZ61eFMYw3Hi2Dvfkz6Ka2ExFLqVbjwUsi4fsu5f5v5FVWk6ajlWt2Tz0CiBT6rQQz64mT5vWidQ4iMJPxIGrlMjOmfpnDoc6ZTJHWJ4-Luvsc9WpnHMbCpVDEKhjq3d9oVgnntSsqAKTLXmFkbfbhiQonoNqXEnOvN0up-InMC-45s9B9986H1zG964VtHco5ASBdvBFH3G49V7ZGSow08A" https://ho.devops.com/products/
<html><head><title>Request Rejected</title></head><body>The requested URL was rejected. Please consult with your administrator.<br><br>Your support ID is: 17698228359465595058<br><br><a href='javascript:history.back();'>[Go Back]</a></body></html>
4. 모든 조건에 충족하는 토큰을 사용할 경우:

위의 토큰을 사용하여 요청 시:
$ curl -k -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im5naW54c3RvcmUifQ.eyJzdWIiOiJOR0lOWCIsIm5hbWUiOiJKb2huIERvZSIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjIsIk5HSU5YU1RPUkUiOiJPSyIsInVzZXIiOiJkZXZvcHNzaGluIiwiZXhwIjoxOTk5OTk5OTk5fQ.uQQZ2iKznQ9L99BhBHofslWZm__r0kNSUxhqNjD75XQ_Yb1oS4qd_joxYPVRYDH9FcjblZUJE0kZqOSao2CJV3lXCeY00JWHj50-rw9JXz8y5IdNkqC0L11S7-lFr-aCo25O5SBt2238kP6ufwAbR8rnUAtCS79d5BzPOKZ-4gkf4qWyCZIM4TWcSR2UCvI4ubBQJQZdCt5Md8FvlNA9CwqP_kE15GqZIgdT26IWu1E3ANtK6fBT3OmJuICLiyonMBGmClSx220HiW2jRfv-K3nFJjVEUGovsA0pD41TbCNV3TY0iGCFxYyADsWtNRdVtQr_ITnKW20q8ZuoNN846w" https://ho.devops.com/products/
[JWT: OK]
정책에 위반한 요청을 NGINX Security Dashboard에서 확인합니다.

또한 claim 값 위조 등의 정황을 security log를 통해 확인할 수 있습니다. 요청한 토큰의 값은 violation_details에 저장됩니다.
<?xml version='1.0' encoding='UTF-8'?>
<BAD_MSG>
<violation_masks>
<block>414000000000c00-3a03030c30000072-f200000000000000-0</block>
<alarm>475f0ffcb9d0fea-befbf35cb000007e-f600000000000000-0</alarm>
<learn>0-0-0-0</learn>
<staging>0-0-0-0</staging>
</violation_masks>
<request-violations>
<violation>
<viol_index>134</viol_index>
<viol_name>VIOL_ACCESS_UNAUTHORIZED</viol_name>
<authorization_rule>users</authorization_rule>
<uri>L3Byb2R1Y3RzLw==</uri>
<jwt_claims>
{
"sub": "NGINX",
"name": "John Doe",
"admin": true,
"iat": 1516239022,
"NGINXSTORE": "Valid",
"user": "devopssss",
"exp": 1999999999
}
</jwt_claims>
</violation>
<violation>
<viol_index>93</viol_index>
<viol_name>VIOL_RATING_THREAT</viol_name>
</violation>
</request-violations>
</BAD_MSG>
위와 같이 VIOL_ACCESS_INVALID 등으로 차단된 것을 확인할 수 있습니다.
3. 결론
F5 WAF for NGINX를 활용하면 JWT Protection으로 토큰 검증을 강화할 수 있습니다. accessProfile과 authorizationRules를 통해 유연한 방어가 가능합니다.
설정 예시를 기반으로 환경에 맞게 조정하세요. NGINX Instance Manager로 실시간 모니터링하면 API 보안을 강화할 수 있습니다.
F5 WAF for NGINX의 JWT Protection 기능에 대해 더 궁금하시면, NGINX STORE에 연락하여 자세한 정보를 확인해 보세요.
댓글을 달려면 로그인해야 합니다.