Kong | Vault 연동 테스트

Kong에서 JWT Plugin을 사용하여 토큰 인증 구성을 해두었다.
DB less mode라 secret key를 yaml에 하드코딩 해야하는 부분이 있어,
이 부분을 harshicorp의 Vault를 이용해보았다.

Kong은 OpenSource를 사용하고 있어 vault plugin을 사용 할 수 없다.
그래서 lua커스텀 플러그인을 구현해야한다.

Vault

UI, CLI 또는 HTTP API를 사용하여 토큰, 비밀번호, 인증서, 비밀을 보호하는 암호화 키 및 기타 민감한 데이터에 대한 액세스를 보호하고 저장하며 엄격하게 제어하는 도구이다. 아직 사용방법은 잘 모르겠다.

설치방법

이번 포스팅에서는 Docker를 이용하여 배포하고 백엔드 저장소는 구성하지 않았다.

vault-data폴더는 없으면 알아서 생성된다.

vault-config폴더는 생성하고 내부에 config.hcl파일을 설정한다.

Vault Config

# docker-vault/vault-config/config.hcl

storage "file" {
  path = "/vault/file"
}

listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_disable = 1
}

disable_mlock = true
ui = true

Docker-compose

version: "3.8"

services:
  vault:
    image: hashicorp/vault:1.15
    container_name: vault-file
    ports:
      - "8200:8200"
    cap_add:
      - IPC_LOCK
    environment:
      VAULT_ADDR: "http://0.0.0.0:8200"
    volumes:
      - ./vault-data:/vault/file
      - ./vault-config:/vault/config
    command: vault server -config=/vault/config/config.hcl

Secret 등록

5개의 키를 생성하고 3개의 키를 인증해야 해제되도록 구성 합니다.

생성된 키를 다운 받아서 보관합니다. 분실되면 복구할수없음

Token 1개 key 5개를 확인 할수 있는데, root token은 테스트를 위해서 vault 인증에 사용한다.

키를 넣으면 우측하단에 카운트 되기 시작한다.

Unseal 후 Root token으로 접속한다.

Secrets Engines를 선택하고 Enable new egine을 눌러 등록한다.

Key-Value를 선택

Path를 넣고 생성한다.

# 추후 새성 형태 예시:
https://127.0.0.1:8200/v1/django-secret/data/jwt-key
항목설명
Path for this secretSecret이 저장될 하위 경로 (예: jwt-key 입력 시 /django-secret/data/jwt-key 로 저장됨)
Secret data"key": "value" 형식의 데이터
JSON 모드여러 개의 key-value를 JSON으로 한 번에 입력 가능 (스위치 On)

Kong 설정

테스트 환경에서는 db less 설정으로 구성해서 테스트해보고있다.

plugins 등록

kong의 plugins이 저장된 경로이다.

/usr/local/share/lua/5.1/kong/plugins/*

기본적으로 제공되는 Kong OSS bundled(plugin)리스트를 확인 할수있다.
(그래도 혹시 모르니 사용전에 라이선스 확인을 해야 할것 같긴 하다…)

acl, acme, aws-lambda, azure-functions, basic-auth 등...

custom plugin은 사용하기 위해서 kong 설정 파일에 plugins 항목에 추가해야한다.
주석처리된 내용을 보면 plugins는 기본적으로 plugins = bundled로 설정되어 있다.

테스트에서 새로추가할 플러그인의 이름은 vault-jwt이다.
plugins = bundled, vault-jwt 로 등록한다.

등록만하고 서버는 아직 적용 하지 않는다.

vi /etc/kong/kong.conf
# plugin add
# default: bundled
plugins = bundled, vault-jwt

vault-jwt폴더 하위에 handler.lua와 schema.lua를 생성한다.

  • handler.lua – Vault에서 공개키를 가져오고 JWT 서명 검증
  • schema.lua – 설정 항목 정의 (vault_addr, vault_token, vault_path)
$ sudo chown -R kong:kong /usr/local/share/lua/5.1/kong/plugins/vault-jwt

lua는 좀 생소한 언어지만, ChatGPT가 잘 도와줘서 꾸역꾸역 구현했다..

schema.lua 정의

# schema.lua 
return {
  name = "vault-jwt",
  fields = {
    { config = {
        type = "record",
        fields = {
          { vault_addr = { type = "string", required = true }, },
          { vault_token = { type = "string", required = true }, },
          { vault_path = { type = "string", required = true }, },
          { vault_key_name = { type = "string", required = true }, },
        },
      },
    },
  }
}

handler.lua 정의

log생성 설정을 꼭 하도록 한다. 그래야 error발생시 error.log에 나타난다.

# 로그 위치
/usr/local/kong/logs/*

handler설정은 아래와 같다.

jwt.claims으로 설정해야하는데 jwt.payload로 구성해서 문제가 있었다…
(이부분은 추후 개선해야 될수도 있을 것 같다. base64인코딩으로 들어오는 것을 받아서 변환해야 될것 같기도…)
들어오는 형태를 보고 판단해야해서 로그설정이 필요하다.

# handler.lua
local jwt_decoder = require "kong.plugins.jwt.jwt_parser"
local http = require "resty.http"
local cjson = require "cjson.safe"

local VaultJWTHandler = {
  PRIORITY = 1000,
  VERSION = "1.0"
}

function VaultJWTHandler:access(conf)
  -- 1. Authorization 헤더 확인
  local auth_header = kong.request.get_header("Authorization")
  if not auth_header then
    ngx.log(ngx.ERR, "[vault-jwt] Missing Authorization header")
    return kong.response.exit(401, { message = "Missing Authorization header" })
  end

  -- 2. Bearer 토큰 추출
  local token = auth_header:match("Bearer%s+(.+)")
  if not token then
    ngx.log(ngx.ERR, "[vault-jwt] Invalid Bearer token format: ", auth_header)
    return kong.response.exit(401, { message = "Invalid Bearer token format" })
  end
  ngx.log(ngx.ERR, "[vault-jwt] Received JWT token: ", token)

  -- 3. JWT 디코딩
  local jwt, err = jwt_decoder:new(token)
  if not jwt or not jwt.claims then
    ngx.log(ngx.ERR, "[vault-jwt] JWT parsing failed. Error: ", err or "unknown", " | jwt: ", cjson.encode(jwt))
    return kong.response.exit(401, { message = "Invalid JWT" })
  end

  ngx.log(ngx.ERR, "[vault-jwt] Decoded JWT claims: ", cjson.encode(jwt.claims))

  -- 4. Vault에서 secret 가져오기
  local httpc = http.new()
  local res, err = httpc:request_uri(conf.vault_addr .. "/v1/" .. conf.vault_path, {
    method = "GET",
    headers = {
      ["X-Vault-Token"] = conf.vault_token,
    }
  })

  if not res then
    ngx.log(ngx.ERR, "[vault-jwt] Vault request error: ", err)
    return kong.response.exit(500, { message = "Vault request error: " .. err })
  end

  local body = cjson.decode(res.body)
  local secret = body and body.data and body.data.data and body.data.data[conf.vault_key_name]
  if not secret then
    ngx.log(ngx.ERR, "[vault-jwt] Failed to parse secret from Vault response: ", res.body)
    return kong.response.exit(500, { message = "Failed to parse secret from Vault" })
  end

  -- 5. JWT 서명 검증
  local ok, err = jwt:verify_signature(secret, "HS256")
  if not ok then
    ngx.log(ngx.ERR, "[vault-jwt] Invalid JWT signature: ", err)
    return kong.response.exit(401, { message = "Invalid JWT signature" })
  end

  -- 6. Kong에게 인증 처리 알리기 (consumer 매핑)
  local iss = jwt.claims.iss or "anonymous"
  kong.client.authenticate(nil, {
    id = iss,
    custom_id = iss
  })

  -- 7. 부가적인 헤더 추가 가능
  kong.service.request.set_header("X-Verified-Sub", jwt.claims.sub or "")
end

return VaultJWTHandler

kong.yml 설정

Plugin을 등록하는 과정이 필요하다.
Plugin을 등록하고 적용할 Service와 매핑하면된다.

(기존에 jwt plugin에 하드코딩하던 것을 대체 할수 있다.그런데 결국 vault인증 정보가 들어있어야되는데…이게 맞나?)

plugins:
  - name: vault-jwt # kong.conf에 등록된 plugins 이름
    service: demo-be-blog-create
    config:
      vault_addr: http://192.168.0.24:8200
      vault_token: hvs.4RyaIVtbARBRZgNWVrA9cuvk
      vault_path: django-secret/data/jwt-key
      vault_key_name: django-secret

  - name: vault-jwt # kong.conf에 등록된 plugins 이름
    service: demo-be-blog-detail
    config:
      vault_addr: http://192.168.0.24:8200
      vault_token: hvs.4RyaIVtbARBRZgNWVrA9cuvk
      vault_path: django-secret/data/jwt-key
      vault_key_name: django-secret

주의사항

vault는 재부팅하면 unseal과정이 필요합니다.

웹접속후 해제하면 정상 동작됨.

이번 포스팅은 여기까지 입니다~!