이 게시물은 Microservices March 2023의 개념을 실제로 구현하는 데 도움이 되는 4가지 튜토리얼 중 하나입니다. 마이크로서비스 제공 시작하기 :
많은 마이크로서비스 에는 안전하게 작동하기 위한 비밀이 필요합니다. 비밀의 예로는 SSL/TLS 인증서의 개인 키, 다른 서비스를 인증하는 데 사용하는 API 키, 원격 로그인을 위한 SSH 키 등이 있습니다. 적절한 비밀 관리를 위해서는 비밀이 사용되는 맥락을 꼭 필요한 곳으로만 엄격히 제한하고, 필요한 경우를 제외하고는 비밀에 접근하지 못하도록 해야 합니다. 하지만 이런 관행은 애플리케이션 개발의 급박함 속에서 종종 생략됩니다. 결과는? 부적절한 비밀 관리로 인해 정보 유출 및 악용이 자주 발생합니다.
이 튜토리얼에서는 클라이언트 컨테이너가 서비스에 액세스하는 데 사용하는 JSON 웹 토큰(JWT)을 안전하게 배포하고 사용하는 방법을 보여줍니다. 이 튜토리얼의 4가지 과제에서는 비밀을 관리하는 4가지 다른 방법을 실험하여 컨테이너에서 비밀을 올바르게 관리하는 방법뿐만 아니라 부적절한 방법에 대해서도 배웁니다.
이 튜토리얼에서는 JWT를 샘플 비밀로 사용하지만, 이 기술은 데이터베이스 자격 증명, SSL 개인 키, 기타 API 키 등 비밀을 유지해야 하는 컨테이너의 모든 것에 적용됩니다.
이 튜토리얼은 두 가지 주요 소프트웨어 구성 요소를 활용합니다.
GET
요청을 하는 매우 간단한 Python 코드를 실행하는 컨테이너튜토리얼이 실제로 어떻게 적용되는지 보여주는 영상을 시청해 보세요.
이 튜토리얼을 수강하는 가장 쉬운 방법은 Microservices March 에 등록하고 제공되는 브라우저 기반 랩을 사용하는 것입니다. 이 게시물에서는 사용자 환경에서 튜토리얼을 실행하는 방법에 대한 지침을 제공합니다.
자신의 환경에서 튜토리얼을 완료하려면 다음이 필요합니다.
nano
나 vim
과 같은 텍스트 편집기curl
(대부분 시스템에 이미 설치되어 있음)git
(대부분 시스템에 이미 설치되어 있음)참고사항:
docker
run
명령으로 테스트 서버를 시작할 때 ‑p
플래그를 사용하여 다른 값을 설정합니다. 그 다음에 다음을 포함합니다. :<포트 번호>
접미사 로컬호스트
에서 컬
명령.~
)는 홈 디렉토리를 나타냅니다.이 섹션에서는 튜토리얼 저장소를 복제하고 , 인증 서버를 시작하고 , 토큰을 포함하거나 포함하지 않고 테스트 요청을 보냅니다 .
홈 디렉토리에서 microservices-march 디렉토리를 만들고 여기에 GitHub 저장소를 복제합니다. (다른 디렉토리 이름을 사용하고 이에 따라 지침을 조정할 수도 있습니다.) 저장소에는 구성 파일과 비밀을 얻기 위해 다양한 방법을 사용하는 API 클라이언트 애플리케이션의 별도 버전이 포함되어 있습니다.
mkdir ~/microservices-marchcd ~/microservices-march
git 복제 https://github.com/microservices-march/auth.git
비밀을 보여주세요. 이는 서명된 JWT로, 일반적으로 API 클라이언트를 서버에 인증하는 데 사용됩니다.
고양이 ~/마이크로서비스-3월/인증/apiclient/토큰1.jwt "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2Nz UyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
이 토큰을 사용하여 인증하는 방법은 여러 가지가 있지만, 이 튜토리얼에서는 API 클라이언트 앱이 OAuth 2.0 Bearer Token Authorization 프레임워크를 사용하여 이를 인증 서버에 전달합니다. 여기에는 JWT 앞에 Authorization이라는 접두사를 붙이는 것이 포함됩니다.
교군꾼
이 예와 같이:
"권한 부여: 운반자 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA"
인증 서버 디렉토리를 변경합니다:
cd apiserver
인증 서버에 대한 Docker 이미지를 빌드합니다(마지막 기간에 주의하세요):
docker build -t apiserver .
인증 서버를 시작하고 실행 중인지 확인합니다(출력은 읽기 쉽도록 여러 줄로 나뉩니다).
docker run -d -p 80:80 apiserver docker ps 컨테이너 ID 이미지 명령어 ...
2b001f77c5cb apiserver "nginx -g '데몬의..." ... ... 생성 상태 ... ... 26초 전 26초 전 ... ... 항구 ... ... 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp ... ... 이름 ... relaxation_proskuriakova
JWT를 포함하지 않는 요청을 인증 서버가 거부하고 반환하는지 확인하십시오. 401
필요한
승인
:
curl -X GET http://localhost<html>
<head><title>401 권한 부여 필요</title></head>
<body>
<center><h1>401 권한 부여 필요</h1></center>
<hr><center>nginx/1.23.3</center>
</body>
</html>
Authorization
헤더를 사용하여 JWT를 제공합니다. 그만큼200
OK
반환 코드는 API 클라이언트 앱이 성공적으로 인증되었음을 나타냅니다.
curl -i -X GET -H "권한 부여: 전달자 `cat $HOME/microservices-march/auth/apiclient/token1.jwt`" http://localhost HTTP/1.1 200 OK 서버: nginx/1.23.2 날짜: 일 , 일 월 YYYY hh : mm : ss TZ 콘텐츠 유형: text/html 콘텐츠 길이: 64 최종 수정: 일 , DD 월 YYYY hh : mm : ss TZ 연결: keep-alive ETag: "63dc0fcd-40" X-메시지: 성공 apiKey1 Accept-Ranges: bytes { "response": "success", "authorized": true, "value": "999" }
이 과제를 시작하기 전에 분명히 해두자면, 앱에 비밀을 하드코딩하는 것은 끔찍한 생각입니다! 컨테이너 이미지에 액세스할 수 있는 사람이라면 누구나 하드코딩된 자격 증명을 쉽게 찾아 추출할 수 있는 방법을 살펴보겠습니다.
이 챌린지에서는 API 클라이언트 앱의 코드를 빌드 디렉토리에 복사하고 , 앱을 빌드하고 실행한 후 비밀을 추출합니다 .
apiclient 디렉토리의 app_versions 하위 디렉토리에는 4가지 과제에 대한 간단한 API 클라이언트 앱의 여러 버전이 포함되어 있으며, 각 버전은 이전 버전보다 약간 더 안전합니다(자세한 내용은 튜토리얼 개요를 참조하세요).
API 클라이언트 디렉토리로의 변경:
cd ~/microservices-march/auth/apiclient
이 과제를 위해 하드코딩된 비밀이 있는 앱을 작업 디렉토리로 복사합니다.
cp ./app_versions/very_bad_hard_code.py ./app.py
앱을 살펴보세요:
cat app.py urllib.request 가져오기 urllib.error 가져오기 jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" authstring = "베어러 " + jwt req = urllib.request.Request("http://host.docker.internal") req.add_header("인증", authstring) 시도: urllib.request.urlopen(req)를 응답으로 사용: the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message) except urllib.error.URLError as e: print(str(e.code) + " s " + e.msg)
이 코드는 단순히 로컬 호스트에 요청을 하고 성공 메시지나 실패 코드를 출력합니다.
요청은 이 줄에 Authorization
헤더를 추가합니다.
req.add_header("권한 부여", authstring)
다른 점이 눈에 띄나요? 아마도 하드코딩된 JWT가 있을까요? 이 부분에 대해서는 곧 다루겠습니다. 먼저 앱을 빌드하고 실행해 보겠습니다.
Docker Compose YAML 파일과 함께 docker
compose
명령을 사용합니다. 이렇게 하면 무슨 일이 일어나고 있는지 이해하기가 조금 더 쉬워집니다.
(이전 섹션의 2단계에서 챌린지 1과 관련된 API 클라이언트 앱의 Python 파일( very_bad_hard_code.py )의 이름을 app.py 로 변경했습니다. 나머지 세 가지 과제에서도 이런 일을 하게 될 겁니다. app.py를 매번 사용하면 Dockerfile을 변경할 필요가 없으므로 물류가 간소화됩니다. (즉, 컨테이너를 매번 강제로 다시 빌드하려면 docker
compose
명령에 ‑build
인수를 포함해야 한다는 의미입니다.)
docker
compose
명령은 컨테이너를 빌드하고, 애플리케이션을 시작하고, 단일 API 요청을 한 다음 컨테이너를 종료하고 콘솔에 API 호출의 결과를 표시합니다.
그만큼200
출력의 마지막에서 두 번째 줄에 있는 성공
코드는 인증이 성공했음을 나타냅니다. apiKey1
값은 인증 서버가 JWT에서 해당 이름의 클레임을 디코딩할 수 있었음을 보여주므로 추가 확인입니다.
docker compose -f docker-compose.hardcode.yml up -build ... apiclient-apiclient-1 | 200 성공 apiKey1 apiclient-apiclient-1이 코드 0으로 종료되었습니다.
따라서 하드코딩된 자격 증명은 API 클라이언트 앱에서 정상적으로 작동했습니다. 놀라운 일이 아닙니다. 하지만 안전한가요? 그럴 수도 있겠죠. 컨테이너는 종료되기 전에 이 스크립트를 한 번만 실행하고 셸이 없으니까요.
사실, 전혀 안전하지 않습니다.
하드코딩된 자격 증명을 사용하면 컨테이너 이미지에 액세스할 수 있는 모든 사람이 이를 검토할 수 있습니다. 컨테이너의 파일 시스템을 추출하는 것은 간단한 작업이기 때문입니다.
추출 디렉토리를 만들고 다음과 같이 변경합니다.
mkdir 추출cd 추출
컨테이너 이미지에 대한 기본 정보를 나열합니다. --format
플래그는 출력을 더 읽기 쉽게 만들어줍니다(같은 이유로 여기서는 출력이 두 줄로 분산되어 있습니다):
docker ps -a --format "테이블 {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" 컨테이너 ID 이름 이미지 ...
11b73106fdf8 apiclient-apiclient-1 apiclient ... ad9bdc05b07c exciting_clarke apiserver ... ... 생성 상태 ... 6분 전 종료 (0) 4분 전 ... 43분 전 업 43분
가장 최신의 apiclient 이미지를 .tar 파일로 추출합니다. 을 위한 <컨테이너_ID>
, 값을 대체합니다 컨테이너
ID
위 출력의 필드(11b73106fdf8
이 튜토리얼에서는):
docker export -o api.tar <컨테이너_ID >
컨테이너의 전체 파일 시스템을 포함하는 api.tar 아카이브를 만드는 데 몇 초가 걸립니다. 비밀을 찾는 한 가지 방법은 전체 아카이브를 추출하여 분석하는 것이지만, 흥미로운 내용을 찾는 지름길이 있습니다. 바로 docker
history
명령으로 컨테이너의 기록을 표시하는 것입니다. (이 단축키는 Docker Hub나 다른 컨테이너 레지스트리에서 찾은 컨테이너에도 작동하므로 Dockerfile 이 없고 컨테이너 이미지만 있는 경우에도 특히 유용합니다.)
컨테이너의 기록을 표시합니다.
docker history apiclient 이미지가 생성됨 ...
9396dde2aad0 8분 전 ... <누락됨> 8분 전 ... <누락됨> 28분 전 ... ... 크기에 따라 생성됨 ... ... CMD ["python" "./app.py"] 622B ... ... 복사 ./app.py ./app.py # buildkit 0B ... ... 작업 디렉터리 /usr/app/src 0B ... ... COMMENT ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0 ... buildkit.dockerfile.v0
출력 줄은 역순으로 정렬되어 있습니다. 이들은 작업 디렉토리가 /usr/app/src 로 설정되었고, 그 다음 앱의 Python 코드 파일이 복사되어 실행되었음을 보여줍니다. 이 컨테이너의 핵심 코드베이스가 /usr/app/src/app.py 에 있고, 따라서 자격 증명이 여기에 있을 가능성이 높다는 것을 추론하는 데는 뛰어난 탐정이 필요하지 않습니다.
해당 지식을 바탕으로 해당 파일만 추출합니다.
tar --추출 --파일=api.tar usr/app/src/app.py
파일의 내용을 표시하면, 그렇게 해서 "보안" JWT에 액세스할 수 있게 됩니다.
고양이 usr/app/src/app.py ... jwt="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA4MTMsImlzcyI6ImFwaUtleTEiLCJhdWQiOiJhcGlTZXJ2aWNlIiwic3ViIjoiYXBpS2V5MSJ9._6L_Ff29p9AWHLLZ-jEZdihy-H1glooSq_z162VKghA" ...
2023년 3월 마이크로서비스 단원 1(마이크로서비스 아키텍처에 Twelve‑Factor 앱 적용)을 완료했다면 환경 변수를 사용하여 구성 데이터를 컨테이너에 전달하는 방법에 익숙할 것입니다. 놓치셨다면 걱정하지 마세요. 등록 후 주문형으로 시청할 수 있습니다.
이 챌린지에서는 비밀을 환경 변수로 전달합니다. 챌린지 1 의 방법과 마찬가지로 이 방법도 권장하지 않습니다! 하드코딩된 비밀만큼 나쁘지는 않지만 보시다시피 몇 가지 약점이 있습니다.
컨테이너에 환경 변수를 전달하는 방법은 네 가지가 있습니다.
Dockerfile 에서 ENV
명령문을 사용하여 변수 대체를 수행합니다(빌드된 모든 이미지에 대한 변수를 설정합니다). 예를 들어:
환경 포트 $PORT
docker
run
명령에서 ‑e
플래그를 사용합니다. 예를 들어:
docker run -e 비밀번호=123 mycontainer
환경
키를 사용합니다.이번 챌린지에서는 환경 변수를 사용하여 JWT를 설정하고 컨테이너를 조사하여 JWT가 노출되었는지 확인합니다.
API 클라이언트 디렉토리로 다시 변경하세요.
cd ~/microservices-march/auth/apiclient
환경 변수를 사용하는 이 과제를 위한 앱을 작업 디렉토리로 복사하여 과제 1의 app.py 파일을 덮어씁니다.
cp ./app_versions/medium_environment_variables.py ./app.py
앱을 살펴보세요. 출력의 관련 줄에서 비밀(JWT)은 로컬 컨테이너의 환경 변수로 읽힙니다.
cat app.py ... jwt = "" os.environ에 "JWT"가 있는 경우: jwt = "Bearer " + os.environ.get("JWT") ...
위에서 설명한 대로 환경 변수를 컨테이너로 가져오는 방법에는 여러 가지가 있습니다. 일관성을 위해 Docker Compose를 고수하겠습니다. JWT
환경 변수를 설정하기 위해 환경
키를 사용하는 Docker Compose YAML 파일의 내용을 표시합니다.
cat docker-compose.env.yml --- 버전: "3.9" 서비스: apiclient: 빌드: . 이미지: apiclient extra_hosts: - "host.docker.internal:host-gateway" 환경: - JWT
환경 변수를 설정하지 않고 앱을 실행합니다. 그만큼401
출력의 두 번째 마지막 줄에 있는 인증되지 않은
코드는 API 클라이언트 앱이 JWT를 통과하지 못해 인증에 실패했음을 확인합니다.
docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 401 권한이 없는 apiclient-apiclient-1이 코드 0으로 종료되었습니다.
간편하게 하려면 환경 변수를 로컬로 설정하세요. 지금 당장은 보안 문제가 우려되지 않으므로 튜토리얼의 이 시점에서 그렇게 해도 괜찮습니다.
JWT 내보내기=`cat token1.jwt`
컨테이너를 다시 실행합니다. 이제 테스트는 챌린지 1과 동일한 메시지와 함께 성공합니다.
docker compose -f docker-compose.env.yml up -build ... apiclient-apiclient-1 | 200 성공 apiKey1 apiclient-apiclient-1이 코드 0으로 종료되었습니다.
따라서 적어도 이제 기본 이미지에는 비밀이 포함되지 않으므로 런타임에 전달할 수 있으므로 더 안전합니다. 하지만 아직도 문제가 있습니다.
API 클라이언트 앱의 컨테이너 ID를 얻기 위해 컨테이너 이미지에 대한 정보를 표시합니다(출력은 읽기 쉽도록 두 줄로 나뉩니다).
docker ps -a --format "테이블 {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" 컨테이너 ID 이름 이미지 ...
6b20c75830df apiclient-apiclient-1 apiclient ... ad9bdc05b07c exciting_clarke apiserver ... ... 생성 상태 ... 6분 전 종료 (0) 6분 전 ... 약 1시간 전 업 약 1시간 전
API 클라이언트 앱의 컨테이너를 검사합니다. <컨테이너_ID>
, 값을 대체합니다 컨테이너
ID
위 출력의 필드(여기 6b20c75830df
).
docker
inspect
명령을 사용하면 현재 실행 중인지 여부에 관계없이 시작된 모든 컨테이너를 검사할 수 있습니다. 문제는 컨테이너가 실행 중이 아니더라도 출력에서 Env
배열의 JWT가 노출되고 컨테이너 구성에 안전하지 않은 방식으로 저장된다는 것입니다.
docker inspect <컨테이너 ID> ...
"Env": [ "JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZ24ifQ.eyJpYXQiOjE2NzUyMDA...", "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C.UTF-8", "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", "PYTHON 버전=3.11.2", "PYTHON_PIP 버전=22.3.1", "PYTHON_SETUPTOOLS 버전=65.5.1", "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...", "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..." ]
이제 여러분은 비밀을 하드코딩하고 환경 변수를 사용하는 것이 여러분(또는 여러분의 보안 팀)이 필요로 하는 만큼 안전하지 않다는 것을 알게 되었을 것입니다.
보안을 강화하려면 로컬 Docker 비밀을 사용하여 중요한 정보를 저장해보세요. 다시 말하지만, 이것은 금본위의 방법은 아니지만 그 작동 방식을 이해하는 것이 좋습니다. 프로덕션에서 Docker를 사용하지 않더라도 중요한 점은 컨테이너에서 비밀을 추출하기 어렵게 만들 수 있다는 것입니다.
Docker에서 비밀은 /run/secrets/ 파일 시스템 마운트를 통해 컨테이너에 노출되며, 여기에는 각 비밀의 값이 포함된 별도의 파일이 있습니다.
이번 챌린지에서는 Docker Compose를 사용하여 로컬에 저장된 비밀을 컨테이너에 전달한 다음, 이 방법을 사용할 때 비밀이 컨테이너에 표시되지 않는지 확인합니다 .
이제 예상했을 수도 있듯이 apiclient 디렉토리로 변경하는 것으로 시작합니다.
cd ~/microservices-march/auth/apiclient
컨테이너 내부의 비밀을 사용하는 이 챌린지의 앱을 작업 디렉토리로 복사하여 챌린지 2의 app.py 파일을 덮어씁니다.
cp ./app_versions/better_secrets.py ./app.py
/run/secrets/jot 파일에서 JWT 값을 읽는 Python 코드를 살펴보세요. (그리고 물론, 파일에 한 줄만 있는지 확인해야 할 것입니다. (아마도 2024년 3월에 마이크로서비스로 출시될 예정인가요?)
cat app.py ... jotfile = "/run/secrets/jot" jwt = "" if os.path.isfile(jotfile): with open(jotfile) as jwtfile: for line in jwtfile: jwt = "Bearer " + line ...
좋아요. 그러면 이 비밀을 어떻게 만들까요? 답은 docker-compose.secrets.yml 파일에 있습니다.
Docker Compose 파일을 살펴보면 비밀 파일은 secrets
섹션에 정의되어 있고 apiclient
서비스에서 참조됩니다.
cat docker-compose.secrets.yml --- 버전: "3.9" 비밀: jot: 파일: token1.jwt 서비스: apiclient: 빌드: .extra_hosts: - "host.docker.internal:host-gateway" 비밀: - jot
앱을 실행합니다. 컨테이너 내에서 JWT에 액세스할 수 있게 만들었기 때문에 인증은 이제 익숙한 메시지와 함께 성공합니다.
docker compose -f docker-compose.secrets.yml up -build ... apiclient-apiclient-1 | 200 성공 apiKey1 apiclient-apiclient-1이 코드 0으로 종료되었습니다.
API 클라이언트 앱의 컨테이너 ID를 기록하여 컨테이너 이미지에 대한 정보를 표시합니다(샘플 출력은 챌린지 2의 컨테이너 검사 에서 1단계 참조):
docker ps -a --format "테이블 {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
API 클라이언트 앱의 컨테이너를 검사합니다. <컨테이너_ID>
, 값을 대체합니다 컨테이너
ID
이전 단계의 출력에 있는 필드입니다. 컨테이너 검사 의 2단계 출력과 달리 Env
섹션 시작 부분에는 JWT=
줄이 없습니다.
docker inspect <컨테이너_ID> "Env": [ "PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LANG=C.UTF-8", "GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D", "PYTHON_VERSION=3.11.2", "PYTHON_PIP_VERSION=22.3.1", "PYTHON_SETUPTOOLS_VERSION=65.5.1", "PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1a96dc5acd0303c4700e026...", "PYTHON_GET_PIP_SHA256=d1d09b0f9e745610657a528689ba3ea44a73bd19c60f4c954271b790c..." ]
지금까지는 순조로웠지만, 비밀은 /run/secrets/jot 에 있는 컨테이너 파일 시스템에 있습니다. 아마도 챌린지 1의 컨테이너 이미지에서 비밀을 검색하는 것과 같은 방법을 사용하여 거기에서 비밀을 추출할 수 있을 것입니다.
추출 디렉토리(챌린지 1에서 생성한 디렉토리)로 변경하고 컨테이너를 tar 아카이브로 내보냅니다.
cd extractdocker export -o api2.tar <컨테이너_ID >
tar 파일에서 secrets 파일을 찾으세요:
타르 tvf api2.tar | grep jot -rwxr-xr-x 0 0 0 0 Mon DD hh :mm 실행/비밀/jot
아, JWT가 포함된 파일이 보이네요. 컨테이너에 비밀을 내장하는 것이 "안전하다"고 말하지 않았나요? 상황이 챌린지 1과 마찬가지로 나쁠까요?
살펴보겠습니다. tar 파일에서 secrets 파일을 추출하여 내용을 살펴보세요.
tar --extract --file=api2.tar 실행/비밀/jotcat 실행/비밀/jot
좋은 소식이에요! cat
명령의 출력이 없으므로 컨테이너 파일 시스템의 run/secrets/jot 파일이 비어 있습니다. 그 안에는 볼 만한 비밀이 없습니다! 컨테이너에 비밀 아티팩트가 있더라도 Docker는 컨테이너에 민감한 데이터를 저장하지 않을 만큼 똑똑합니다.
그런데 이 컨테이너 구성이 안전하더라도 하나의 단점이 있습니다. 컨테이너를 실행할 때 로컬 파일 시스템에 token1.jwt 라는 파일이 있는지 여부에 따라 달라집니다. 파일 이름을 바꾸면 컨테이너를 다시 시작하려는 시도가 실패합니다. ( token1.jwt 의 이름을 [삭제하지 않고!] 바꾸고 1단계의 docker
compose
명령을 다시 실행하여 직접 시도해 볼 수 있습니다.)
이제 절반은 달성한 셈입니다. 컨테이너는 비밀을 쉽게 침해되지 않도록 보호하는 방식으로 비밀을 사용하지만, 비밀은 여전히 호스트에서 보호되지 않습니다. 비밀이 암호화되지 않은 채 일반 텍스트 파일에 저장되는 것은 원하지 않을 것입니다. 이제 비밀 관리 도구를 도입할 때입니다.
비밀 관리자는 비밀의 수명 주기 전반에 걸쳐 비밀을 관리, 검색하고 순환하는 데 도움이 됩니다. 선택할 수 있는 비밀 관리자는 매우 다양하며 모두 비슷한 목적을 달성합니다.
비밀 관리에 대한 옵션은 다음과 같습니다.
단순화를 위해 이 과제에서는 Docker Swarm을 사용하지만 원칙은 많은 비밀 관리자에서 동일합니다.
이 챌린지에서는 Docker에서 비밀을 생성하고 , 비밀과 API 클라이언트 코드를 복사하고 , 컨테이너를 배포하고 , 비밀을 추출 할 수 있는지 확인하고, 비밀을 회전시킵니다 .
지금까지의 관례에 따라 apiclient 디렉토리로 변경합니다.
cd ~/microservices-march/auth/apiclient
Docker Swarm 초기화:
docker swarm init Swarm 초기화됨: 현재 노드(t0o4eix09qpxf4ma1rrs9omrm)는 이제 관리자입니다. ...
비밀을 생성하여 token1.jwt 에 저장합니다.
도커 시크릿 생성 jot ./token1.jwt qe26h73nhb35bak5fr5east27
비밀에 대한 정보를 표시합니다. 비밀 값(JWT) 자체는 표시되지 않는다는 점에 유의하세요.
docker secret inspect jot [ { "ID": "qe26h73nhb35bak5fr5east27", "버전": { "색인": 11 }, "생성된 시간": " YYYY - MM - DD T hh : mm : ss . ms Z", "UpdatedAt": " YYYY - MM - DD T hh : mm : ss . ms Z", "Spec": { "Name": "jot", "Labels": {} } } ]
API 클라이언트 애플리케이션 코드에서 Docker 비밀을 사용하는 것은 로컬에서 생성된 비밀을 사용하는 것과 정확히 동일합니다. /run/secrets/ 파일 시스템에서 읽을 수 있습니다. Docker Compose YAML 파일에서 비밀 한정자만 변경하면 됩니다.
Docker Compose YAML 파일을 살펴보세요. 외부
필드에서 true
값을 확인하면 Docker Swarm 비밀을 사용하고 있음을 나타냅니다.
cat docker-compose.secretmgr.yml --- 버전: "3.9" 비밀: jot: 외부: true 서비스: apiclient: 빌드: . 이미지: apiclient extra_hosts: - "host.docker.internal:host-gateway" 비밀: - jot
따라서 이 Compose 파일이 기존 API 클라이언트 애플리케이션 코드와 함께 작동할 것으로 예상할 수 있습니다. 음, 거의 그렇죠. Docker Swarm(또는 다른 컨테이너 오케스트레이션 플랫폼)은 많은 추가적인 가치를 제공하지만, 약간의 추가적인 복잡성도 초래합니다.
docker
compose는
외부 비밀과 작동하지 않으므로 docker
stack
deploy
와 같은 Docker Swarm 명령을 사용해야 합니다. Docker Stack은 콘솔 출력을 숨기므로 출력을 로그에 기록한 다음 로그를 검사해야 합니다.
작업을 더 쉽게 하기 위해, 컨테이너가 계속 실행되도록 while
True
연속 루프를 사용합니다.
이번 챌린지의 앱(비밀 관리자를 사용하는 앱)을 작업 디렉토리로 복사하여 챌린지 3의 app.py 파일을 덮어씁니다. app.py 의 내용을 표시하면 해당 코드가 챌린지 3의 코드와 거의 동일하다는 것을 알 수 있습니다. 유일한 차이점은 while
True
루프가 추가되었다는 것입니다.
cp ./app_versions/best_secretmgr.py ./app.pycat ./app.py ... while True: time.sleep(5) try: with urllib.request.urlopen(req) as response: the_page = response.read() message = response.getheader("X-MESSAGE") print("200 " + message, file=sys.stderr) except urllib.error.URLError as e: print(str(e.code) + " " + e.msg, file=sys.stderr)
컨테이너를 빌드합니다(이전 과제에서는 Docker Compose가 이 작업을 처리했습니다):
docker build -t apiclient .
컨테이너 배포:
docker stack deploy --compose-file docker-compose.secretmgr.yml secretstack 네트워크 secretstack_default 생성 서비스 secretstack_apiclient 생성
실행 중인 컨테이너를 나열하고 secretstack_apiclient 에 대한 컨테이너 ID를 기록합니다(앞과 마찬가지로 가독성을 위해 출력이 여러 줄에 걸쳐 분산되어 있습니다).
docker ps --format "테이블 {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}" 컨테이너 ID ...
20d0c83a8b86 ... ad9bdc05b07c ... ... 이름 ... ... secretstack_apiclient.1.0e9s4mag5tadvxs6op6lk8vmo ... ... exciting_clarke ... ... 이미지 생성 상태 ... apiclient:latest 31초 전 Up 30초 ... apiserver 2시간 전 Up 2시간
Docker 로그 파일을 표시합니다. <컨테이너_ID>
, 값을 대체합니다 컨테이너
ID
이전 단계의 출력에 있는 필드(여기서는 20d0c83a8b86
). 로그 파일에 일련의 성공 메시지가 표시되는데, 이는 애플리케이션 코드에 while
True
루프를 추가했기 때문입니다. 명령을 종료하려면 Ctrl+c를
누르세요.
도커 로그 -f <컨테이너_ID>200 성공 apiKey1
200 성공 apiKey1
200 성공 apiKey1
200 성공 apiKey1
200 성공 apiKey1
200 성공 apiKey1
...
^씨
민감한 환경 변수가 설정되어 있지 않다는 것은 알고 있습니다(하지만 2번째 과제의 컨테이너 검사 2단계에서처럼 docker
inspect
명령으로 항상 확인할 수 있습니다).
챌린지 3에서 우리는 /run/secrets/jot 파일이 비어 있다는 사실도 알고 있지만 다음을 확인할 수 있습니다.
cd extractdocker export -o api3.tar
tar --extract --file=api3.tar 실행/비밀/jot
cat 실행/비밀/jot
성공! 컨테이너에서 비밀을 가져올 수도 없고 Docker 비밀에서 직접 읽을 수도 없습니다.
물론, 적절한 권한이 있다면 서비스를 생성하고 비밀을 로그로 읽거나 환경 변수로 설정하도록 구성할 수 있습니다. 더불어, API 클라이언트와 서버 간의 통신이 암호화되지 않았다는 점(일반 텍스트)을 눈여겨보셨을 것입니다.
그러므로 거의 모든 비밀 관리 시스템에서는 비밀이 유출될 가능성이 있습니다. 결과적으로 발생할 수 있는 손상 가능성을 제한하는 한 가지 방법은 비밀을 정기적으로 순환(교체)하는 것입니다.
Docker Swarm을 사용하면 비밀을 삭제한 다음 다시 생성할 수만 있습니다(Kubernetes는 비밀의 동적 업데이트를 허용합니다). 실행 중인 서비스에 연결된 비밀도 삭제할 수 없습니다.
실행 중인 서비스를 나열하세요.
docker 서비스 ls ID 이름 모드 ... sl4mvv48vgjz secretstack_apiclient 복제 ... ... 복제본 이미지 포트 ... 1/1 apiclient:최신
secretstack_apiclient 서비스를 삭제합니다.
도커 서비스 rm secretstack_apiclient
비밀을 삭제하고 새 토큰으로 다시 생성합니다.
docker secret rm jot
docker secret create jot ./token2.jwt
서비스를 다시 생성합니다.
docker stack 배포 --compose-file docker-compose.secretmgr.yml secretstack
apiclient
에 대한 컨테이너 ID를 찾습니다(샘플 출력은 컨테이너 배포 및 로그 확인 의 3단계 참조):
docker ps --format "테이블 {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.RunningFor}}\t{{.Status}}"
일련의 성공 메시지를 보여주는 Docker 로그 파일을 표시합니다. 을 위한 <컨테이너_ID>
, 값을 대체합니다 컨테이너
ID
이전 단계의 출력에 있는 필드입니다. 명령을 종료하려면 Ctrl+c를
누르세요.
도커 로그 -f <컨테이너_ID>200 성공 apiKey2
200 성공 apiKey2
200 성공 apiKey2
200 성공 apiKey2
...
^씨
apiKey1
에서 apiKey2
로 변경된 것을 보시나요? 비밀을 돌렸군요.
이 튜토리얼에서는 API 서버는 여전히 두 JWT를 모두 허용하지만, 프로덕션 환경에서는 JWT의 클레임에 대한 특정 값을 요구하거나 JWT의 만료 날짜를 확인하여 이전 JWT를 더 이상 사용하지 않을 수 있습니다.
비밀을 업데이트할 수 있는 비밀 시스템을 사용하는 경우 코드는 새로운 비밀 값을 선택하기 위해 비밀을 자주 다시 읽어야 합니다.
이 튜토리얼에서 만든 객체를 정리하려면:
secretstack_apiclient 서비스를 삭제합니다.
도커 서비스 rm secretstack_apiclient
비밀을 삭제하세요.
도커 시크릿 rm jot
(이 튜토리얼을 위해 떼를 생성했다고 가정하고) 떼를 떠나세요.
도커 스웜 떠나기 --force
실행 중인 apiserver 컨테이너를 종료합니다.
docker ps -a | grep "apiserver" | awk {'print $1'} |xargs docker kill
원치 않는 컨테이너를 나열한 후 삭제합니다.
docker ps -a --format "테이블 {{.ID}}\t{{.이름}}\t{{.이미지}}\t{{.실행중}}\t{{.상태}}"docker rm <컨테이너_ID>
원치 않는 컨테이너 이미지를 나열하고 삭제하여 삭제합니다.
도커 이미지 목록 도커 이미지 rm <이미지_ID>
이 블로그를 활용하여 귀하의 환경에 튜토리얼을 구현하거나 브라우저 기반 랩에서 시도해 볼 수 있습니다( 여기에서 등록하세요 ). Kubernetes 서비스 노출 주제에 대해 자세히 알아보려면 2단원의 다른 활동을 따라하세요. 마이크로서비스 비밀 관리 101 .
NGINX Plus를 사용한 프로덕션 등급 JWT 인증에 대해 자세히 알아보려면 설명서를 확인하고 블로그에서 JWT 및 NGINX Plus를 사용하여 API 클라이언트 인증하기를 읽어보세요.
"이 블로그 게시물에는 더 이상 사용할 수 없거나 더 이상 지원되지 않는 제품이 참조될 수 있습니다. 사용 가능한 F5 NGINX 제품과 솔루션에 대한 최신 정보를 보려면 NGINX 제품군을 살펴보세요. NGINX는 이제 F5의 일부가 되었습니다. 이전의 모든 NGINX.com 링크는 F5.com의 유사한 NGINX 콘텐츠로 리디렉션됩니다."