[keycloak] TinyRadius로 RADIUS 서버 만들기(feat. netty)
ruinnel
1678 Words CHANGE ME READ TIME 7 Minutes, 37 Seconds
2019-07-21 07:58 +0000
TinyRadius 란?
- 원래 Sourceforge에 있던 프로젝트를 fork 한걸로 보입니다.
- Server와 Client를 모두 포함하고 있습니다.
- RadiusServer class는 추상(
abstract
)클래스라서 상속 후 필요한 부분을 구현하고 서버를 시작하면 됩니다. - TestServer.java / TestClient.java를 제공하고 있어 참고해서 구현하면 되고, TestClient로 테스트도 용이합니다.
서비스에 사용하기 위해 수정
- 성능과 안정성을 위해 Netty 서버를 기동하려고 합니다.
- RadiusServer class을 상속해 인증처리를 스트래티지 패턴(
strategy pattern
)을 적용해 다양한 인증 방법을 사용 할 수 있도록 구조화 합니다. - Radius 프로토콜의
Access Challenge
를 이용해 OTP 입력 요청 처리를 포함 시킵니다. - Docker로 실행하는 것을 상정하고 환경변수로 다양한 동작 설정이 가능하도록 합니다.
0. 들어가기 전에…
- 전체 소스는 https://github.com/ruinnel/netty-radius-server 에 있습니다.
1. Netty로 기동을 위해 Handler로 작성
- RadiusServerHandler 작성.
- io.netty.channel.ChannelInboundHandlerAdapter를 상속 Radius Packet 처리가 가능한 Handler를 구현합니다.
{{ }} graph TD; CLIENT[client] –>|패킷수신| CHANNEL[channel] subgraph Netty CHANNEL –> RADIUS_SERVER_HANDLER[RadiusServerHandler] RADIUS_SERVER_HANDLER –> RADIUS_PACKET_PROCESSOR[RadiusPacketProcessor] RADIUS_PACKET_PROCESSOR –> AUTHENTICATOR[Authenticator] subgraph 인증 모듈 AUTHENTICATOR –>|인증| IDP[Identifier Provider] end end end {{ }}
2. RadiusPacketProcessor
- Tiny Radius의 Radius Server를 상속 받아
인증을 Authenticator Interface를 이용하도록 구현하고, OTP 입력 요청 처리도 구현합니다.
accessRequestReceived
메소드를 구현하고getUserPassword
메소드는 빈 메소드로 구현합니다.- Tiny Radius의 RadiusServer 클래스를 상속하면 원래
getUserPassword
를 구현하게 되고, 그 반환값이 사용자의 입력값과 동일한지 체크 하는 방식으로 인증을 처리 합니다. - 이렇게하면 ID제공자(Identifier Provider)를 외부에 둘 경우 제약이 많아지므로
getUserPassword
를 호출 하기 전 단계인accessRequestReceived
메소드를 override 해서 다시 구현했습니다.
- Tiny Radius의 RadiusServer 클래스를 상속하면 원래
override fun accessRequestReceived(accessRequest: AccessRequest, client: InetSocketAddress): RadiusPacket {
val nasId = accessRequest.getAttribute("NAS-Identifier")?.attributeValue
val username = accessRequest.userName
var password = accessRequest.userPassword
var otp = ""
val state = accessRequest.getAttribute("State")?.attributeData
var answer: RadiusPacket
logger.debug("useOtp - ${authenticator.useOtp(nasId)}, otp - ${otp}")
if (authenticator.useOtp(nasId)) {
if (state != null && password != null) {
otp = password
password = String(codec.decrypt(state), Charsets.UTF_8)
}
// access challenge - request otp
if (otp.isEmpty()) {
answer = RadiusPacket(RadiusPacket.ACCESS_CHALLENGE, accessRequest.packetIdentifier)
answer.addAttribute(RadiusAttribute(answer.dictionary.getAttributeTypeByName("State").typeCode, codec.encrypt(password!!))) // State
answer.addAttribute("Reply-Message", authenticator.replyMessage())
copyProxyState(accessRequest, answer)
return answer
}
}
logger.debug("accessRequestReceived - nasId: ${nasId} / username: ${username} / password: ${password} / otp: ${otp}")
try {
answer = if (authenticator.authenticate(username!!, password!!, otp)) {
RadiusPacket(RadiusPacket.ACCESS_ACCEPT, accessRequest.packetIdentifier)
} else {
RadiusPacket(RadiusPacket.ACCESS_REJECT, accessRequest.packetIdentifier)
}
} catch (e: RuntimeException) {
answer = RadiusPacket(RadiusPacket.ACCESS_REJECT, accessRequest.packetIdentifier)
logger.warn("authorization fail - $username")
}
copyProxyState(accessRequest, answer)
return answer
}
override fun getUserPassword(userName: String?): String {
// never called.
return ""
}
3. Authenticator
- 아래 Interface를 구현해서 인증을 처리합니다.
authenticate
메소드의 반환값에 따라 인증 성공/실패 여부가 처리 됩니다.useOtp
를true
로 반환할 경우 OTP 입력을 위한Access-Challenge
요청을 보내게 됩니다.secretKey
는useOtp
를 사용할 경우 실제로 Radius Packet이 서버/클라이언트 간에 2회 오가게 되는데, 첫번째는 password가 포함된 패킷을 수신하고Access-Challenge
를 반환 하면 두번째 otp가 포함된 패킷을 수신하게 됩니다.- 이때
Access-Challenge
패킷을 반환할때 password를 이 secretKey로 암호화 해서 state attribute에 넣어서 반환합니다. - state attribute의 경우 client에서 수신하면 다음 요청시에 받은 값을 그대로 보내도록 되어 있어 otp 입력값을 수신할때 state attribute에 암호화된 패스워드가 들어가 있는 형태가 됩니다.
- 보안 강화를 위해 state에 session key 같은 key값을 반환하고 서버의 메모리에 첫번째 입력받은 password를 유지하는 방법도 있습니다만, 서버를 여러대 사용할 경우 redis 등 외부 메모리 db를 고려해야 할 수 있습니다. 구조를 단순하게 가고 의존성을 줄이기 위해 이런 방식을 사용 했습니다.
replyMessage
는Access-Challenge
패킷을 반환할때 사용자에게 표시되는 OTP 입력 요청 메세지에 사용할 문자열을 설정 합니다.
interface Authenticator {
fun authenticate(username: String, password: String, otp: String?): Boolean
fun useOtp(nasIdentifier: String?): Boolean
fun secretKey(): String
fun replyMessage(): String
}
4. Application.kt & NettyRadiusServer.kt
Application.kt
는 각종 환경변수 및 default 값에 대한 처리.NettyRadiusServer.kt
는 netty에 Radius Server 동작을 위한 handler를 설정하고 기동 처리를 합니다.