[keycloak] TinyRadius로 RADIUS 서버 만들기(feat. netty)


TinyRadius 란?

  • 원래 Sourceforge에 있던 프로젝트를 fork 한걸로 보입니다.
  • Server와 Client를 모두 포함하고 있습니다.
  • RadiusServer class는 추상(abstract)클래스라서 상속 후 필요한 부분을 구현하고 서버를 시작하면 됩니다.
  • TestServer.java / TestClient.java를 제공하고 있어 참고해서 구현하면 되고, TestClient로 테스트도 용이합니다.

서비스에 사용하기 위해 수정

  • 성능과 안정성을 위해 Netty 서버를 기동하려고 합니다.
  • RadiusServer class을 상속해 인증처리를 스트래티지 패턴(strategy pattern)을 적용해 다양한 인증 방법을 사용 할 수 있도록 구조화 합니다.
  • Radius 프로토콜의 Access Challenge를 이용해 OTP 입력 요청 처리를 포함 시킵니다.
  • Docker로 실행하는 것을 상정하고 환경변수로 다양한 동작 설정이 가능하도록 합니다.

0. 들어가기 전에…

1. Netty로 기동을 위해 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 해서 다시 구현했습니다.
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 메소드의 반환값에 따라 인증 성공/실패 여부가 처리 됩니다.
  • useOtptrue로 반환할 경우 OTP 입력을 위한 Access-Challenge 요청을 보내게 됩니다.
  • secretKeyuseOtp를 사용할 경우 실제로 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를 고려해야 할 수 있습니다. 구조를 단순하게 가고 의존성을 줄이기 위해 이런 방식을 사용 했습니다.
  • replyMessageAccess-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를 설정하고 기동 처리를 합니다.

관련글 목록

1. Keycloak를 이용한 SSO 구축

2. Keycloak 설치하기(with Kubernetes)

3. RADIUS 서버 설치 및 Keycloak 연동 설정

4. Linux 서버의 SSH(+ sudo) 인증 설정

5. EAP-TTLS & PAP 접속 설정

번외. Custom Linux PAM을 이용해 Keycloak 계정으로 ssh, sudo 인증 하기

번외. TinyRadius로 RADIUS 서버 만들기(feat. netty)

comments powered by Disqus