LoadBalancer, K8S Client를 이용한 ACL 서비스

강준혁·2023년 3월 13일
0

외부 접속 허용을 관리하기 위해 ACL를 관리하는게 일반적입니다.

VPN을 사용하면 되지만 작은 스타트업에서는 일반적으로 너무너무 비싸서 사내 직원분들에게 외부에서도 쉽게 ip 등록하는 서비스를 제공해 운영했던 방법을 알려드립니다.

AWS EKS를 사용하고 있으며 K8S의 지식을 어느정도 알고 있다는 전제로 작성합니다.

작동 원리

먼저 Load balancer를 생성 합니다.

어떤 방식으로든 상관없습니다 ingress로 만드셔도 됩니다.

여기서 저는 ingress-nginx를 통해 만들었습니다. (https://github.com/kubernetes/ingress-nginx)

(ALB 를 사용하는 경우 아래 링크를 참조)
https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.2/guide/ingress/annotations/

저는 AWS를 사용하고 있어 aws 에 load balancer가 만들어졌습니다.

그리고 K8S에서 지원하는 loadBalancerSourceRanges 옵션을 활용합니다.

service에서 사용이 가능합니다.

해당 ranges에 cidr를 추가하면 whitelist가 활성화되어 해당 ip만 원하는 pod로 접근하는게 가능합니다. (security group에 의해 제어)

원리는 알았으니 등록하는 서비스만 만들면 됩니다.

ip 등록 서비스

K8S client (https://github.com/kubernetes-client/java) 로 등록 서비스를 만듭니다.
(k8s client 사용법은 따로 설명하지 않겠습니다.)

K8S는 기본적으로 http api 를 지원하므로 다른 방식으로도 충분히 구현할 수 있습니다.

@RestController
@RequestMapping("/acl")
class AclController (private val aclService: AclService) {

    @GetMapping
    fun registration(httpServletRequest: HttpServletRequest): String {
        return aclService.registrationIp(httpServletRequest)
    }
}

spring web으로 외부에서 접근할 수 있는 Controller를 만듭니다.

Load balancer로 들어오는 client ip를 알기위해 httpservletrequest 파라미터도 가져옵니다.
nginx가 있으므로 헤더값에서 client ip를 가져올 수 있습니다. (alb는 바로 가져올 수 있음)

@Service
class AclService(private val coreV1Api: CoreV1Api, private val slackService: SlackService) {
    private val namespace = "ingress"
    private val ingressNginx = "ingress-nginx-controller"
    private val apiClient = coreV1Api.apiClient
    
    fun registrationIp(httpServletRequest: HttpServletRequest): String {
        println("registration start")
        val ingressService = coreV1Api.readNamespacedService(ingressNginx, namespace, null)
        val ip = httpServletRequest.getHeader("X-Forwarded-For") ?: httpServletRequest.remoteAddr
        val ranges = ingressService.spec!!.loadBalancerSourceRanges!!
        val cidr = "$ip/32"

        if (ranges.contains(cidr)) return "이미 등록된 ip 입니다."

        ranges.add(cidr)

        patch(ingressService.metadata!!.name!!, V1Patch(apiClient.json.serialize(ingressService)))
        println("patch success: $cidr")
        slackService.sendMessage("$cidr 가 등록 되었습니다.")

        val executors = Executors.newScheduledThreadPool(1)
        executors.schedule({
            removeIp(cidr)
        }, 1, TimeUnit.HOURS)

        return "정상 등록되었습니다."
    }
    
    fun removeIp(cidr: String) {
        println("remove start")

        val ingressService = coreV1Api.readNamespacedService(ingressNginx, namespace, null)
        val ranges = ingressService.spec!!.loadBalancerSourceRanges!!
        ranges.remove(cidr)

        patch(ingressService.metadata!!.name!!, V1Patch(apiClient.json.serialize(ingressService)))

        println("remove ip: $cidr")
    }

    private fun patch(name: String, patch: V1Patch) {
        PatchUtils.patch(
            V1Service::class.java,
            { coreV1Api.patchNamespacedServiceCall(name, namespace, patch, null, null, null, null, null, null) },
            V1Patch.PATCH_FORMAT_STRATEGIC_MERGE_PATCH, apiClient
        )
    }
}

저는 등록과 함께 슬랙을 보내는 부분이 존재하지만 이부분은 상황에 맞게 조정 하시면 됩니다.

그리고 1시간 후 자동으로 ip가 제거되도록 executor thread를 만들어줍니다.

등록이 되면 svc의 옵션에 ip가 추가됩니다. (cidr 16은 예시)

접근 권한

만약 ip 등록 서비스가 올라간 ns에 ingress nginx의 접근 권한이 없다면 role을 추가해 주면 됩니다.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: service-patch
rules:
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["patch"]
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: service-patch
subjects:
  - kind: ServiceAccount
    name: ingress-nginx
roleRef:
  kind: Role
  name: service-patch
  apiGroup: rbac.authorization.k8s.io

Ingress nginx에는 sa가 존재하므로 해당 sa에 role 권한을 k8s에 올릴때 ip 등록 서비스에 같이 주시면 됩니다.

외부 접근 보안

저는 spring security를 통해 로그인으로 접속할 수 있도록 했습니다.

다른 여러 방법으로 security를 추가하면 될 것 같습니다.

감사합니다.

0개의 댓글