안녕하세요? 무스마 이선임입니다.

2020년도 벌써 반이 넘어갑니다.

지난 거의 절반을 몹쓸 COVID-19와 함께 지내왔는데요. 그럼에도 불구하고, 주변에 아픈 분이 없어서 참 다행입니다.

그동안 역대급 큰 프로젝트를 수행하느라 어찌나 바쁘던지 글도 잘 쓰지 못했습니다. 글로 꼭 남기고 싶은 중요한 배움과 정보들이 그동안 많이 쌓였습니다. 이제부터라도 조금씩 쓰면서 정리를 해보려고 합니다.

명색이 연구원이니 아무래도 글을 써야 밥값을 제대로 하는 느낌이 나지요. (배워서 남 주자)

image

AWS IoT 인증서로 임시 보안 자격 증명 발급 받기

오늘은 AWS IoT 인증서임시 보안 자격 증명을 발급받는 방법에 대해서 알아보겠습니다.

목차

용어

먼저 용어부터 알아보겠습니다.

IoT (Internet of Things)

사물인터넷을 뜻하는 말이지만, AWS의 IoT 서비스 영역과 그중 핵심 서비스인 AWS IoT Core를 가리키기도 합니다.

이 글에서는 계속 AWS IoT Core를 뜻합니다.

AWS 서비스 약칭(prefix)으로는 iot입니다.

[개발자 안내서] AWS IoT란 무엇입니까?


인증서 (X.509 Certificates)

일반적으로, 공개키 기반 구조(PKI)에서 사용하는 X.509 인증서를 가리킵니다.

그런데 여기서는 AWS IoT Core에 등록한 인증서를 한정해서 가리킵니다.

이 인증서를 디바이스(사물, Thing)에 연결하거나(attach thing), 특정한 권한을 부여하기 위해 AWS IoT Core에서 관리하는 Policy에 연결(attach policy)할 수 있습니다.

[위키] X.509

AWS IoT Core - MQTT Broker Endpoint

AWS IoT Core는 사물인터넷을 위한 MQTT Broker Endpoint를 제공합니다.

꼭 IoT 용도가 아니더라도, 이것을 마치 MQTT Broker의 관리형 서비스처럼 사용할 수도 있습니다.

이때 클라이언트가 MQTT Endpoint에 접속하고 인증 및 권한을 부여하는 수단으로서 인증서가 사용됩니다.


보안 자격 증명 (Security Credentials)

보안 자격 증명은 한마디로, “내 신원(identity)은 무엇이고, 나는 무엇을 할 권한(authority)이 있다.”를 나타내는 수단입니다.

AWS에 한정해서, 넓은 의미로는 다음의 것들이 포함됩니다.

  • (콘솔) 이메일 / 비밀번호 (Root)
  • (콘솔) IAM User / 비밀번호
  • (콘솔) MFA 인증 (OTP Token)
  • 액세스 키 (ACCESS_KEY_ID / SECRET_ACCESS_KEY)
  • (EC2에 한해서) 키 페어 (SSH)
  • (IoT, 몇몇 서비스) X.509 인증서

그런데 이게 다 보안 자격 증명이라고요? 그렇긴 한데, 뭔가 좀 이상하지요?

Tiobe Index

매달 프로그래밍 언어 인기 순위를 보여주는 Tiobe Index라는 것이 있습니다.

프로그래밍 언어라고 하니 C, Java 같은게 나오면 이해가 가는데, SQL도 나오고 심지어 CSS도 등장합니다.

그것도 프로그래밍 언어냐? 그리고 PHP도 끼워줘야 하나?

네, 그래서 다 필요 없고 좁은 의미로 보안 자격 증명이라고 하면 액세스 키를 떠올려주시면 됩니다.

AWS CLI를 사용하기 위해 발급했던 바로 그거 말입니다.

# 보안 자격 증명 입력
$ aws configure --profile=...

# 보안 자격 증명(액세스 키)이 저장된 파일 보기
$ cat ~/.aws/credentials

따라서 지금부터는 자격 증명이라는 용어 대신, 이하 액세스 키라고 하겠습니다.


임시 보안 자격 증명 (Temporary Security Credentials)

앞서 언급한 것은 IAM 사용자에게 발급되는 영구(Permenant) 보안 자격 증명이었습니다.

한 번 발급하면, 명시적으로 비활성화 하거나 삭제하기 전에는 (반)영구적으로 사용 가능합니다.
(오래 쓰면 바꾸라고 경고가 뜨긴 합니다.)

반면에, 한시적으로만 유효한 임시 보안 자격 증명 (Temporary Security Credentials)이라는 것도 있습니다.

가까운 예를 들면, Amazon Cognito가 있습니다.

인터넷 고객이 웹이나 모바일 앱으로 로그인을 한다고 해봅시다.

인증이 성사되면, Cognito Identity Pool(Federated Identities)에서 sts:AssumeRoleWithWebIdentity API를 호출하여, 해당 사용자로 연결되는 임시 액세스키를 발급 받습니다. (뒤에서 일어나는 일이므로 사용자는 알아차리지 못합니다.)

이것은 인증한 뒤 얼마 동안만 유효한 액세스 키, 즉 임시 보안 자격 증명입니다.

액세스 키 ID 접두어

영구 액세스 키는 AKIAXXXXYYYYZZZZWWWW 같이 AKIA로 시작하는데, 임시 액세스 키는 ASIA아시아?로 시작합니다. 아마 볼 기회가 별로 없었을 것입니다.

영구 액세스 키가 특정 IAM User에 대해 발급돤다면, 임시 액세스 키는 특정한 IAM Role에 대해서 발급됩니다.

위의 Cognito의 경우도 마찬가지로, authenticated, unauthenticated, preferred_role 등 사용자가 미리 지정해 준 Role에 맞게 임시 액세스 키를 발급해줍니다.

[개발자 가이드] Amazon Cognito - 역할 기반 액세스 제어

sts:AssumeRole* 이라고 이름 붙은 모든 API는 “내가 어떤 Role의 권한을 받고 싶은데 임시 액세스 키를 발급해주세요.”라는 의미입니다.

Assume(가정하다) + Role(역할) => 내가 어떤 역할이라고 가정한다. (그러니까 액세스 키를 달라!)

여러분이 AWS에서 새 리소스를 만들 때 마다 왜 자꾸 IAM Role을 같이 만들어 넣으라고 하는지 아셨나요?

그 이유는, 나중에 그 리소스(혹은 서비스)가 sts:AssumeRole을 사용해서 그 IAM Role의 권한이 실체화된 임시 보안 자격 증명을 발급 받아서 다른 서비스나 리소스에 접근하기 때문입니다.

그러니까 IAM Role은 사람도 쓰고, 기계도 쓰고,

AWS 루트 사용자, IAM 사용자, AWS 서비스, 타 Account 사용자, 연합 인증 사용자, 웹에서 로그인 한 사용자 모두가 같은 보안 자격 증명 체계를 사용한다는 뜻이지요.

그러고보면, AWS 인증/권한 시스템은 참 일관성이 있습니다.

[사용 설명서] IAM - 임시 보안 자격 증명


왜 IoT 인증서로 임시 액세스 키를 발급 받으려고 하나?

네, 좋은 질문입니다. 하긴 하는데, 왜 하려는지 이유를 알아야겠지요.

보통 AWS IoT 디바이스에 인증서(ca+cert+key)만 설치해놓고, MQTT만 접속해서 사용해도 웬만한 것을 할 수 있습니다. (default가 default인 이유)

그런데 MQTT 말고도, 다른 AWS 서비스를 직접 사용해야 할 경우도 있을 수 있습니다. 제가 하고 싶어서요

AWS Lambda Function을 직접 호출한다든지 말이지요.

그런데 IoT Endpoint는 인증서(ca+cert+key)로 인증하는 것을 받아주지만,

다른 AWS 서비스는 전형적인 보안 자격 증명액세스 키가 있어야 사용할 수 있습니다.

AWS SDK는 IoT 인증서를 안 받아줍니다. 오직 Credentials를 달라고 하지요.

그렇다고 IAM User를 만들고 액세스 키를 생성해서 디바이스에 갖다 줄 수도 없잖습니까?
(이렇게 하는 사람 왠지 있을 것 같다는… 무서워)

다행히 IoT 인증서를 가지고 임시 액세스 키를 발급해주는 방법이 있습니다.

[개발자 안내서] IoT - AWS 서비스 직접 호출에 대한 권한 부여

만약 이런 방법이 없었다면, 디바이스에 인증서도 넣고, 다른 자격 증명 수단도 넣고 하려면 참 번거롭겠죠?

대신 그냥 인증서를 사용하다가, AWS 서비스 호출이 필요하면 인증서로 임시 액세스 키를 발급받을 수 있으니 얼마나 편리하겠습니까?

그래서 우리는 IoT 인증서로 AWS 임시 액세스 키를 발급 받으려고 합니다.


실습

설명하다가 너무 설명충이 된 것 같네요. 이제 바로 실습을 해보겠습니다.

알면 쉽고 모르면 어려워서 그렇지 이거 대로 따라하면 됩니다.

[개발자 안내서] IoT - AWS 서비스 직접 호출에 대한 권한 부여

인증서 생성

먼저 AWS IoT Core에서 인증서를 생성하고 활성화합니다. (이미 쓰던 것이 있으면 그걸로 해도 됩니다.)

아래의 구성이 한 세트입니다.

  • Certificate (cert.pem)
  • Private Key (private.pem.key)
  • CA Certificate (AmazonRootCA1.pem)

image

Public Key는 없어도 됩니다. 대신 Root CA 파일을 챙겨야 합니다.

(다운로드 버튼이 나란히 있다고 한 세트로 착각하시면 안 됩니다.)


IAM Role 생성

(이 IAM Role의 권한) = (나중에 발급 받을 임시 액세스 키로 가능한 권한)

IAM에 가서 Role을 만들어줍니다.

저는 cdk로 만들었습니다.

import * as iam from '@aws-cdk/aws-iam'

// IAM Role 생성
const someRole = new iam.Role(this, 'someRole', {
  assumedBy: new iam.ServicePrincipal('credentials.iot.amazonaws.com')
})

이것과 같습니다.

image

credentials.iot.amazonaws.com 이라는 AWS 서비스가 이 Role을 사용(assumeRole)할 수 있게 해줍니다. 이렇게 하는 것을 신뢰 관계 설정이라고 합니다.

그리고 필요한 권한 정책을 연결해줍니다.

import * as iam from '@aws-cdk/aws-iam'
import * as lambda from '@aws-cdk/aws-lambda'

const someRole: iam.Role = ...
const someFunc = new lambda.Function(this, 'someFunc', { ... })

// 특정 Lambda 함수 호출 권한 부여
someFunc.grantInvoke(someRole)

이렇게 하면 해당 IAM Role에 권한이 연결됩니다.

이거랑 같습니다.

"Statement": [{
  "Action": "lambda:InvokeFunction",
  "Resource": "arn:aws:lambda:::function:someFuncXXX",
  "Effect": "Allow"
}]


Role Alias 생성

이제 방금 전에 만든 Role을 가리키는 별칭(alias)를 생성합니다.

별칭을 생성하는 이유는, IAM Role을 인증서에 바로 연결하는 대신 별칭을 대신 붙여놓고, IAM Role은 나중에 언제든지 바꿀 수 있게 하기 위함입니다.

image

별칭 이름을 정하고, 아까 만든 Role을 선택해서 연결해줍니다.

image


IoT 디바이스 Policy에 iot:AssumeRoleWithCertificate 권한 추가

IoT 인증서에 연결할 수 있는 Policy가 있습니다.

IAM Policy랑 모양은 똑같은데, AWS IoT Core에서 관리되고 몇가지 IoT 기능이 더 들어가 있습니다.

image

아마 인증서를 사용하면서 iot:Connect, iot:Publish 같은 권한을 넣어둔 적이 있을 것입니다.

iot:* 보안 구멍 쓰는 게으른 사람 없겠죠?

image

이렇게 입력하면 됩니다.

{
  "Action": "iot:AssumeRoleWithCertificate",
  "Resource": "arn:aws:iot:(region):(account):rolealias/(아까 만든 Role Alias))",
  "Effect": "Allow"
}

이 Policy와 연결된 인증서를 갖고 있으면, 그 인증서를 가지고 Role Alias가 가리키는 IAM Role을 assume할 수 있게 됩니다.

즉 임시 액세스 키를 받을 수 있다는 것이지요.


임시 액세스 키 요청하기

AWS 클라우드 안의 서비스였다면 서비스가 자동으로 STS에 요청해서 임시 액세스 키를 받아왔겠지만,

지금의 경우는 디바이스에서 직접 요청을 해서 액세스 키를 받아야 합니다.

그리고 STS Endpoint에 보내는 것이 아니라, IoT Credentials Provider Endpoint로 요청을 보내야합니다.

이 엔드포인트 주소는 공개되어 있지 않은데, AWS CLI로 알아낼 수 있습니다.

$ aws iot describe-endpoint --endpoint-type iot:CredentialProvider

xxxxxxxxxxxxxx.credentials.iot.(region).amazonaws.com

이걸 복사해두고, 나중에 코드에 넣든지 환경변수에 넣든지 해야 합니다.

curl로 되는지 테스트 해봅시다.

$ curl ‐‐cert (cert 파일) --key (key 파일) --cacert (ca 파일) https://(위에서 나온 Endpoint 호스트 이름)/role-aliases/(아까 만든 Role Alias)/credentials

{
  "credentials": {
    "accessKeyId": "ASIAXXXXXXXXXXXX",
    "secretAccessKey": "(secret access key)",
    "sessionToken": "(session token)",
    "expiration": "2020-07-20T09:00:00Z"
  }
}

혹시 오류가 리턴되면 복사-붙여넣기 하지 말고 직접 쳐보세요.

익숙한 accessKeyId, secretAccessKey도 보이고, 임시 액세스 키이므로 유효 기한 expiration도 있습니다.

받았으니 쓰면 되겠지만, 영구 액세스 키가 아니니까 환경변수에 저장해두고 쓰기도 뭣합니다.

그리고 매번 방금 한 것처럼 curl로 명령을 실행해서 받을 수도 없습니다.

[AWS Blog] How to Eliminate the Need for Hardcoded AWS Credentials in Devices by Using the AWS IoT Credentials Provider

튜토리얼 문서가 대개 그렇듯이 무언가 2%, 아니 20%씩 빠져있지요. 쓸만하지가 않아

프로그래밍 방식으로 해야겠지요?


프로그래밍 방식으로 임시 액세스 키 요청하기

TypeScript로 안내해드립니다.

.env - 환경변수 파일을 준비합니다.

AWS_IOT_CREDENTIAL_PROVIDER_ENDPOINT=xxxxxxxxxxxxxxx.credentials.iot.ap-northeast-2.amazonaws.com
AWS_IOT_ROLE_ALIAS=MyRoleAlias

iot-certificate-credentials.ts - AWS SDK의 Credentials를 확장합니다.

import Axios from 'axios'
import * as axios from 'axios'
import * as https from 'https'
import * as mutex from 'async-mutex'
import { AWSError, Credentials } from 'aws-sdk'

export interface IoTCertificateCredentialsProps {
  readonly ca: Buffer
  readonly cert: Buffer
  readonly key: Buffer
  readonly endpoint: string
  readonly roleAlias: string
}

export class IoTCertificateCredentials extends Credentials {
  protected readonly mutex: mutex.Mutex = new mutex.Mutex()
  protected readonly axios: axios.AxiosInstance

  constructor(props: IoTCertificateCredentialsProps) {
    // 처음에는 expired 상태로 시작
    super('', '')
    this.axios = Axios.create({
      httpsAgent: new https.Agent({
        ca: props.ca,
        cert: props.cert,
        key: props.key,
        requestCert: true,
        rejectUnauthorized: true,
      }),
      baseURL: `https://${props.endpoint}/role-aliases/${props.roleAlias}/credentials`,
    })
  }

  /**
   * 자격 증명 만료 15초 전 호출 + 자격 증명 갱신
   */
  public refresh(callback: (err?: AWSError) => void): void {
    this.mutex.acquire().then(async (release) => {
      try {
        if (this.needsRefresh()) {
          const result = await this.axios({ method: 'get' })
          const { credentials } = result.data as IoTCredentialsProviderEndpointResponse
          this.accessKeyId = credentials.accessKeyId
          this.secretAccessKey = credentials.secretAccessKey
          this.sessionToken = credentials.sessionToken
          this.expireTime = new Date(credentials.expiration)
        }
        callback()
      } catch (e) {
        callback(e)
      } finally {
        release()
      }
    })
  }
}

interface IoTCredentialsProviderEndpointResponse {
  credentials: {
    accessKeyId: string
    secretAccessKey: string
    sessionToken: string
    expiration: string // ISO-8601 UTC+Z
  }
}

Credentials.expiration

AWS SDK의 CredentialsexpireTime이 지정될 경우, 만료 시간이 가까우면 자동으로 refresh()를 호출해서 갱신을 시도합니다.

HTTPS Agent

헤더도 아니고, 쿼리 파라미터도 아닌 ca, cert, key를, curl이 아닌 웹 클라이언트로는 어떻게 전송할까요?

바로 HTTPS Agent를 이용하는 겁니다.

Node.js 빌트인 모듈 https로 Agent를 생성할 수 있고, 이것을 웹 클라이언트(여기서는 axios)에 옵션으로 넣을 수 있습니다.

index.ts - 시작

import AWS from 'aws-sdk'
import * as fs from 'fs'

// Globally
AWS.configure.update({
  credentials: new IoTCertificateCredentials({
    ca: fs.readFileSync('ca.pem'),
    cert: fs.readFileSync('cert.pem'),
    key: fs.readFileSync('key.pem'),
    endpoint: String(process.env.AWS_IOT_CREDENTIAL_PROVIDER_ENDPOINT),
    roleAlias: String(process.env.AWS_IOT_ROLE_ALIAS),
  })
})

혹은 이렇게,

import Lambda from 'aws-sdk/clients/lambda'

// 특정 서비스 인스턴스에만 사용
new Lambda({
  credentials: new IoTCertificateCredentials({
    ca: fs.readFileSync('ca.pem'),
    cert: fs.readFileSync('cert.pem'),
    key: fs.readFileSync('key.pem'),
    endpoint: String(process.env.AWS_IOT_CREDENTIAL_PROVIDER_ENDPOINT),
    roleAlias: String(process.env.AWS_IOT_ROLE_ALIAS),
  })
})

사용하시면 됩니다.

위 코드는 MIT License를 따라 제공됩니다. 사용으로 인한 불이익은 책임지지 않습니다.


결론

지금까지 AWS IoT 인증서로 임시 보안 자격 증명, 즉 임시 액세스 키를 발급 받는 방법과 이를 프로그래밍 방식으로 사용하는 방법을 알아보았습니다.

보통 디바이스(Thing)로는 IoT Endpoint와 MQTT를 이용해서 Pub/Sub나 ThingShadow를 쓰지만,

AWS 서비스를 직접 호출해야 할 경우에도 IoT 인증서가 제공하는 같은 보안 수준으로 AWS 서비스에 접근할 수 있게 해줍니다.

오늘 실습한 IoT 인증서와 임시 액세스 키 발급 방법을 이용하면 Machine-to-Machine Authorization에도 쉽게 활용할 수 있습니다.

요약

  • AWS의 자격 증명 수단에는 여러가지가 있지만, 대표적으로 액세스 키가 쓰인다.
    (자격 증명하면, 십중팔구 액세스 키를 가리킴)
  • 액세스 키에는 IAM User에게 발급되는 (영구) 액세스 키와, IAM Role에 대해 발급되는 임시 액세스 키가 있다.
  • IoT 인증서는 IoT Endpoint (MQTT) 인증만 가능하고, 그외 AWS 서비스를 직접 호출하려면 임시 액세스 키를 받아야 하는데, IoT 인증서를 가지고 임시 액세스 키를 발급 받을 수 있다.
  • 보통 임시 액세스 키는 STS Endpoint에 IAM Role을 sts:AssumeRole 해서 받고, IoT 인증서로 받는 임시 액세스 키는 IoT Credentials Provider Endpoint에 Role Alias와 인증서를 전달하여 iot:AssumeRoleWithCertificate 해서 받는다.
  • AWS SDK의 Credentials를 확장하여 IoT 인증서를 사용하는 보안 자격 증명 Provider를 구현할 수 있다.

당부의 말씀

내용이 유익했다면, 아래 링크로 들어가서 각각 따봉👍을 눌러주세요.

AWS IoT가 발전하는데 도움이 됩니다.


References