ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OAuth 2.0 API 보안] API 게이트웨이를 이용한 에지 보안
    보안/OAuth 2.0 2022. 6. 2. 19:07

    API 게이트웨이를 이용한 에지 보안

    API 게이트웨이는 운영 환경의 배치에서 API를 보호하는 가장 일반적인 패턴이다.

    API 배치의 시작점이며 중앙에서 인증, 권한 부여, 제한 정책을 시한하는 정책 시행 지점(PEP)이다.


    1. Zuul API 게이트웨이 설정, 넷플릭스 Zuul API 게이트웨이 배치

    Zuul은 동적 라우팅, 모니터링, 복원력, 보안 등을 제공하는 API 게이트웨이이다.

    • 넷플릭스 서버 인프라의 최전방에 존재하며 트래픽을 처리
    • 요청 라우팅
    • 개발자의 테스팅과 디버깅 지원
    • 넷플릭스 전반적인 서비스의 상태 확인 및 보호
    • 리전 문제 발생시 리전 이동API 서버 실행
    이전과 마찬가지로 API 서버 샘플을 구동하면서 알아본다.

    1.1. Zuul API 게이트웨이 실행

    <!-- pom.xml --> 
    <dependency> 
        <groupId>org.springframework.cloud</groupId> 
        <artifactId>spring-cloud-starter-zuul</artifactId>
    </dependency>

    pom.xml에 zuul 의존성 패키지를 추가한다.

    @EnableZuulProxy
    @SpringBootApplication
    public class GatewayApplication {
    
    ...
    
      public static void main(String[] args) {
          SpringApplication.run(GatewayApplication.class, args);
      }
    
    }
    # application.yaml
    
    zuul:
     routes:
      retail:
       url: "http://localhost:8080" // 백엔드로 라우팅
    • @EnableZuulProxy 어노테이션
    • 스프링 프레임워크가 Zuul 프록시 애플리케이션을 시작하도록 지시한다.

     

    • Zuul API 게이트웨이를 9090번 포트로 실행하고 들어오는 요청을 백엔드의 API 서버로 라우팅한다.
    curl http://localhost:9090/retail/order/11
    위의 요청 결과를 보면 <Zuul_Gateway>/retail/<백엔드 API URI>를 통해 API 게이트웨이가 작동하는 것을 볼 수 있다.

    2. Zuul API 게이트웨이에서 TLS 활성화

    앞서 cURL 클라이언트와 Zuul API 게이트웨이 간의 통신은 HTTP를 이용하였다.

    API 게이트웨이에 TLS를 활성화하여 서비스가 아닌 API 게이트웨이 자체를 보호한다.

    TLS를 이용하기 위해서 공개키/개인키 쌍을 만든다.

    keytool -genkey -alias spring -keyalg RSA -keysize 4096 -validity 3650  
    \-dname "CN=zool,OU=bar,O=zee,L=sjc,S=ca,C=us" -keypass springboot  
    \-keystore keystore.jks -storeType jks -storepass springboot
    # application.yaml
    
    server:  
      ssl:  
        key-store: keystore.jks  
        key-store-password: springboot  
        keyAlias: spring

    키 저장소 파일을 게이트웨이에 적용해보겠다.

    application.yaml 파일에 키 저장소와 접근 패스워드를 등록한다.

    자체 서명을 사용하므로 인증서의 신뢰 유효성을 검증해야한다.

    이에 대한 검증을 하지 않도록 -k 옵션을 전달한다.

    • 공인된 인증서를 사용할 경우 이 옵션은 제외해도 좋다.
    > curl --cacert ca.crt https://localhost:9090/retail/order/11
    > curl: (60) SSL: certificate subject name 'zool' does not match target host name 'localhost'
    
    # 인증서에 dname으로 등록한 zool 도메인 호스트가 일치하지 않음
    
    > curl --cacert ca.crt https://zool:9090/retail/order/11
    
    # zool과 같이 인증서에 등록된 도메인을 등록하고 사용

    3. Zuul API 게이트웨이에서 OAuth 2.0 토큰 유효성 검사 시행

    앞서 Zuul API 게이트웨이를 통해 요청을 프록시하고 TLS를 적용했다.

    추가적인 보안 강화를 위해서 토큰 유효성 검사를 시행한다.

    • 토큰 발급을 위한 OAuth 2.0 인가서버 (보안 토큰 서비스) 필요
    • Zuul API 게이트웨이의 OAuth 토큰 유효성 검사 서비스 필요

    3.1. OAuth 2.0 보안 토큰 서비스 설정

    보안 토큰 서비스(Security Token Service)는 클라이언트에게 토큰을 발행하고 API 게이트웨이의 유효성 검증 요청에 응답한다.

    <!-- pom.xml --> 
    <dependency> 
        <groupId>org.springframework.boot</groupId> 
        <artifactId>spring-boot-starter-security</artifactId> 
    </dependency> 
    <dependency> 
        <groupId>org.springframework.security.oauth</groupId> 
        <artifactId>spring-security-oauth2</artifactId> 
    </dependency>

    위와 같은 종속성 추가로 추가 어노테이션을 사용할 수 있다.
    이를 통해서 스프링부트에서 토큰 유효성 검사를 위한 엔드포인트를 만들 수 있다.
    JWT를 사용하는 경우 이 엔드포인트는 필요없다.

    • @EnableAuthorizationServer : 프로젝트를 OAuth 2.0의 인가 서버로 만든다.
    • @EnableResourceServer : 액세스 토큰의 유효성을 검사하고 사용자 정보를 반환한다.
    @Configuration
    public class AuthServerConfig extends AuthorizationServerConfigurerAdapter{
    
        ...
    
        /* 인가서버에 클라이언트 등록 */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                .withClient("__Client_ID__") // 클라이언트 ID
                .secret("__Client_Secret__") // 클라이언트 암호
                .scopes("foo", "bar") // Scope : Email, Profile 등
                .authorizedGrantTypes("client_credentials", "password", "refresh_token")
                .accessTokenValiditySeconds(6000);
        }
    
        ...
    }

    위와 같이 AuthorizationServerConfigurerAdapter를 상속받아 설정을 오버라이딩 할 수 있다.

    이 설정에서 OAuth 2.0의 요소들을 확인할 수 있으며 위와 같이 클라이언트를 인가서버에 등록한다.

    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        ...
    
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                .withUser("USER_ID")
                .password("USER_PW")
                .roles("USER");
        }
    
        ...
    
    }

    패스워드 승인 방식 지원은 위와 같다.

    인가 서버가 사용자 정보를 저장하는 저장소에 연결되어야한다.

    위의 예시는 특정 사용자를 시스템에 특정 역할로 추가하는 과정이다.

    /* AuthServerConfig */
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) 
        throws Exception {
    
        endpoints.authenticationManager(authenticationManager);
    
    }

    앞선 코드에서 패스워드 승인 방식 지원을 위해 DB와 같은 저장소에 연결했다면 AuthenticationManager를 통해 OAuth 2.0과 연계한다.

     

    3.2. OAuth 2.0 보안 토큰 서비스 테스트

    STS 종단점은 TLS로 보호된다.

    클라이언트 ID와 클라이언트 Secret과 함께 ID, PW를 인가서버로 POST 요청을 보낸다.
    → 이 경우 항상 자신의 자격증명에 접근하므로 필요하지 않다.

    → 자격증명 승인 방식에서는 Refresh Token을 보내지 않는다.

     

    액세스 토큰의 유효성 검사 과정은 다음과 같다.

    1. 자원 서버는 인터셉터를 통해 요청을 가로챈다.
    2. 여기서 액세스 토큰을 추출한다.
    3. 인가 서버와 통신한다.
    // 메타데이터 예시  
    {  
        "details" : {  
            ...  
            "tokenType" : "Bearer",  
            ...  
        },  
        ...  
        ,
    
    
        "name" : "__Client_ID__"
    }
    @RequestMapping("/user")  
    public Principal user(Principal user) {  
        return user;  
    }

    인가서버에 직접 액세스 토큰의 유효성 검사를 요청하면 토큰이 유효한 경우 해당 토큰의 메타데이터를 반환한다. 위의 예시처럼 토큰이 유효하다고 판단되면 user() 메서드에서 빌드된다.

     

    3.3. OAuth 2.0 토큰 유효성 검사를 위한 Zuul API 게이트웨이 설정

    # Zuul API 게이트웨이
    # application.yaml
    
    security:  
     oauth2:  
      resource:  
       user-info-uri: "https://localhost:8443/user"
    keytool -export -alias spring -keystore keystore.jks -storePass springboot -file sts.crt  
    keytool -import -alias sts -keystore keystore.jks -storePass springboot -file sts.crt

    다시 Zuul API 게이트웨이로 돌아와서 인가서버의 HTTPS 종단점을 지정하자.

    또한, 게이트웨이와 인가서버 간의 TLS 연결을 위해 인증서를 키 저장소에 등록해야 한다.

    등록 이후라도 액세스 토큰을 전달하지 않기에 인가는 실패한다.

    클라이언트 ID, 클라이언트 Secret과 ID, Password를 전달한다면 정당하게 유효성 검사 결과를 확인할 수 있다.

    4. Zuul API 게이트웨이와 서비스 간 상호 TLS 활성화

    앞서 확인한 통신 보호는 다음과 같다.

    • cURL 클라이언트와 STS
    • cURL 클라이언트와 Zuul API 게이트웨이
    • Zuul API 게이트웨이와 TLS를 통한 STS 간의 통신
    서비스 요청 보호

    이제 추가적으로 취약한 부분을 보호해야 한다. 위의 그림에서 (4.)서비스 요청의 통신 부분을 보호한다.

    이를 위해선 마찬가지로 게이트웨이와 서비스 간의 상호 TLS를 사용해야한다.

    상호 TLS를 위해 앞서 수행했던 것 처럼 keytool을 통해 공개/비밀키 쌍을 생성한다.

    # API 서비스
    # application.yaml
    
    server:
     ssl:
      key-store: keystore.jks
      key-store-password: springboot
      keyAlias: spring
      client-auth: need # 상호 TLS 사용

    이렇게 상호 TLS를 시행하면 게이트웨이에서는 X.509 인증서로 자체 인증해야하며 API 서비스는 게이트웨이의 X.509 인증서의 인증기관을 신뢰해야 한다.

    → 이를 위해서 Zuul 게이트웨이의 공인 인증서를 API 서비스의 키 저장소로 가져와야 한다.

     

    또한, 게이트웨이도 API 서비스의 공인 인증서에 대한 인증기관을 신뢰해야 한다.

    → 공인된 인증서라면 이미 등록되어 있을 수 있지만 자체 서명 인증서를 사용하므로 마찬가지로 키 저장소에 별도의 등록을 해야 한다.


    5. 독립형 액세스 토큰을 이용한 API 보안

    OAuth 2.0은 토큰 기반 인증을 Bearer 로 식별한다.

    이때의 토큰은 참조형 토큰 혹은 독립형 토큰일 수 있다.

    • 참조형 토큰 : 임의의 문자열, 브루트포싱을 통해 토큰 추측가능
    • 독립형 토큰 : JWT 토큰

    액세스 토큰이 JWT 토큰일 경우 자원 서버는 JWT 서명을 통해 자체적으로 토큰 유효성을 검증할 수 있다.

    5.1. JWT 를 위한 인가 서버 설정

    먼저, 키 저장소와 함께 새로운 키 쌍을 생성한다.
    → 이 키는 인가 서버에서 발행된 JWT를 서명하는데 사용한다.

    # STS 서버
    # application.yaml
    
    spring:
     security:
      oauth:
        jwt: true # JWT Token Store를 토큰 저장소로 사용
         keystore: 
            password: springboot
            alias: jwtkey
            name: jwt.jks
    /* 토큰 저장소 설정 */
    @Bean
    public TokenStore tokenStore() {
    	String useJwt = environment.getProperty("spring.security.oauth.jwt");
    	if (useJwt != null && "true".equalsIgnoreCase(useJwt.trim())) {
    		return new JwtTokenStore(jwtConeverter());
    	} else {
    		return new InMemoryTokenStore();
    	}
    }
    
    /* 개인키 검색을 위한 메서드 */
    @Bean
    protected JwtAccessTokenConverter jwtConeverter() {
    	String pwd = environment.getProperty("spring.security.oauth.jwt.keystore.password");
    	String alias = environment.getProperty("spring.security.oauth.jwt.keystore.alias");
    	String keystore = environment.getProperty("spring.security.oauth.jwt.keystore.name");
    	String path = System.getProperty("user.dir");
    
    	KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
    			new FileSystemResource(new File(path + File.separator + keystore)), pwd.toCharArray());
    	JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    	converter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));
    	return converter;
    }

    위의 코드에서 JwtTokenStore를 토큰 저장소로 설정했다.

    이를 JWT 기반의 인가 흐름에 연계 해야한다.

     

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    	String useJwt = environment.getProperty("spring.security.oauth.jwt");
    	if (useJwt != null && "true".equalsIgnoreCase(useJwt.trim())) {
    		endpoints.tokenStore(tokenStore())
                .tokenEnhancer(jwtConeverter())
                .authenticationManager(authenticationManager);
    	} else {
    		endpoints.authenticationManager(authenticationManager);
    	}
    }

    JWT 흐름을 연계했다면 액세스 토큰의 형식은 기존의 임의의 문자열에서 JWT 형식으로 바뀌어 전달될 것이다. 이는 Base64로 인코딩 된 형태다.

    JWT는 헤더, 페이로드, 시그너처로 이루어져 있다. 시그너처는 서명을 위해 사용된다.

    client_credentials 승인 방식이라면 JWT에는 승인에 대한 정보 외 사용자 이름은 포함되지 않는다.

     

    5.2. JWT 를 이용한 API 게이트웨이 보호

    # API Gateway
    # application.yaml
    
    security:
     oauth2:
      resource:
       jwt:
        keyUri: "https://localhost:8443/oauth/token_key"

    이 설정값은 인가 서버에서 JWT를 서명하는데 사용된 개인키에 대응되는 공개키를 제공한다.

    이 공개키를 통해 API 게이트웨이가 JWT의 서명을 확인할 수 있다.

     

    마찬가지로 유효한 토큰이라면 정당한 응답을 받을 수 있다.

     


    6. WAF

    결국 API 게이트웨이는 인증, 인가, 제한에 대한 정책을 시행하는 지점(PEP)이다.

    하지만 말 그대로 요청 자체가 정당하고 내부의 인가 토큰과 같은 부분에서의 검증을 수행한다.

    외부에서 들어오는 공격을 막기 위해서는 방화벽과 같은 추가 수단을 배치해야 한다.

     

    필요한 요소를 융합하여 복합적으로 배치하여 심층 방어를 구성해야 한다.

    댓글

Designed by Tistory.