Private Docker Registry 구축 및 보안 강화 -(2)

안상운·2025년 2월 16일
2

Docker

목록 보기
14/14
post-thumbnail

✅ 단계 1: Docker Private Registry 기본 구축

• registry:2 공식 이미지를 활용하여 로컬 Private Registry 컨테이너 실행
• docker tag 및 docker push 명령어를 사용하여 이미지를 Private Registry에 업로드
• docker pull을 통해 저장된 이미지 정상 다운로드 확인

1. 로컬 Private Registry 컨테이너 실행

Docker에서 제공하는 registry:2 공식 이미지를 사용하여 Private Registry 컨테이너를 실행.

docker run -d --name registry -p 5000:5000 --restart=always registry:2

-d: 컨테이너를 백그라운드 모드로 실행.
-p 5000:5000: 호스트의 5000 포트를 컨테이너의 5000 포트에 매핑하여 접근할 수 있게 함.
--name registry: 컨테이너 이름을 registry로 지정.

5000 포트 listen 유무 확인.

2. 이미지를 Private Registry에 업로드하기 위한 태깅

로컬에 있는 이미지를 Private Registry에 업로드하기 전에, 이미지에 새로운 태그를 붙여서 Registry 주소를 포함시켜야 합니다. 예를 들어, 로컬에 my-app:latest라는 이미지가 있다면 아래와 같이 태깅:

docker tag hello-world:latest [registry가 위치한 머신의 주소:5000]/hello-world:latest

3. 이미지 업로드

docker push [registry가 위치한 머신의 주소:5000]/hello-world:latest
  • 에러 발생.
Get "https://192.168.0.31:5000/v2/": http: server gave HTTP response to HTTPS client

이 오류 메시지는 Docker 클라이언트가 기본적으로 HTTPS를 사용하여 레지스트리에 접근하려고 하는데,
실제 레지스트리 서버는 HTTPS가 아닌 HTTP로 동작하고 있기 때문에 발생.

  • 해결방법.
    Insecure Registry 설정 사용
    만약 레지스트리를 HTTPS로 구성하지 않았다면, Docker 데몬에 해당 레지스트리를 "insecure registry"로 설정.

Linux의 경우: /etc/docker/daemon.json 파일에 다음 내용을 추가합니다.

{
  "insecure-registries": ["192.168.0.31:5000"]
}

systemctl restart docker

  • push
docker push [registry가 위치한 머신의 주소:5000]/hello-world:latest

4. 이미지 다운로드 (Pull)로 정상 저장 확인

마지막으로, Private Registry에 업로드된 이미지가 정상적으로 저장되었는지 확인하기 위해 다른 시스템이나 동일 시스템에서 이미지를 내려받음.

✅ 단계 2: Registry 보안 설정 (TLS/SSL 인증 적용)

• OpenSSL을 사용하여 자체 서명된 SSL 인증서 생성
• 생성한 인증서를 Registry에 적용하여 HTTPS 기반 통신 구성
• curl을 활용하여 TLS 적용 여부 확인

*볼륨을 사용하여 Registry 데이터를 영속적으로 저장

docker run -d --name registry -p 5000:5000 --restart=always --mount type=volume,source=my-volume,target=/var/lib/registry registry:2

이제 컨테이너를 삭제했다가 실행해도 내용이 데이터가 날아가지 않음.

1. OpenSSL을 사용하여 자체 서명된 SSL 인증서 생성

openssl req \
  -newkey rsa:4096 -nodes -sha256 \
  -keyout private_registry.key \
  -x509 -days 365 \
  -out private_registry.crt
  • newkey rsa:4096: 4096비트 RSA 키 쌍을 생성합니다.
  • nodes: 개인키를 암호 없이 생성합니다.
  • sha256: SHA-256 해시 알고리즘을 사용합니다.
  • x509: X.509 인증서를 생성합니다.
  • days 365: 인증서 유효 기간을 365일로 설정합니다.

2. 생성한 인증서를 Registry에 적용하여 HTTPS 기반 통신 구성

docker run -d --name registry -p 5000:5000 \
--restart=always \
--mount type=volume,source=my-volume,target=/var/lib/registry \
-v /home/evan/cert:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/private_registry.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/private_registry.key \
registry:2

3. curl을 활용하여 TLS 적용 여부 확인

curl -v https://192.168.0.31:5000/v2/

  • pull

이와 같이 OpenSSL을 이용한 인증서 생성, Registry에 인증서 적용, 그리고 curl을 통한 확인 과정을 통해 Docker Registry의 보안을 강화할 수 있음.

✅ 단계 3: 기본 인증(Basic Authentication) 설정

• htpasswd를 사용하여 사용자 계정을 생성하고 인증 적용
• docker login 명령어를 활용하여 인증 확인
• 인증 없이 접근 시 차단되는지 테스트

1. htpasswd를 사용하여 사용자 계정 생성

  • htpasswd란?
    Apache HTTP Server에서 사용하는 인증 파일을 생성하는 도구로, Docker Registry에도 기본 인증을 적용할 때 사용됩니다.

  • 계정 생성

mkdir auth
docker run --entrypoint htpasswd httpd:2 -Bbn evan evan > auth/htpasswd

-B 옵션은 bcrypt 알고리즘을 사용하여 암호화.
-bn 옵션은 배치 모드로, 프롬프트 없이 username과 password를 바로 받아 처리.
결과로 생성된 auth/htpasswd 파일에 인증 정보가 저장.

실행되는 컨테이너가 보이지 않는 이유는 이 명령어는 단순히 htpasswd 도구를 실행하여 인증 파일을 생성하기 위해 컨테이너를 일회성으로 실행하는 것.

일회성 실행:
docker run 명령어는 지정된 커맨드를 실행한 후 종료. htpasswd 명령어가 실행되어 인증 정보를 출력하면, 그 결과를 auth/htpasswd 파일로 리디렉션하여 저장한 후 컨테이너는 종료됨.

컨테이너가 백그라운드에서 계속 실행되는 것이 아님:
이 경우에는 장기적으로 실행되어야 하는 서비스가 아니라, 단순히 필요한 데이터를 생성하기 위한 도구로 사용되었으므로, 실행 후 종료되는 것이 정상.

2. Registry에 기본 인증 적용 (TLS/SSL 인증과 별개로 적용)

docker run -d --name registry -p 5000:5000 \
--restart=always \
--mount type=volume,source=my-volume,target=/var/lib/registry \
-v /home/evan/cert:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/private_registry.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/private_registry.key \
-v $(pwd)/auth:/auth \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd" \
registry:2
  • -v $(pwd)/auth:/auth: 현재 디렉토리의 auth 폴더를 컨테이너 내부의 /auth로 마운트합니다.
  • REGISTRY_AUTH=htpasswd: 기본 인증 사용을 지정.
  • REGISTRY_AUTH_HTPASSWD_REALM: 인증 영역(Realm)을 설정합니다. 클라이언트에 인증 창이 표시될 때 이 이름이 보여짐.
  • REGISTRY_AUTH_HTPASSWD_PATH: 컨테이너 내에서 인증 파일의 경로를 지정.

3. docker login 명령어를 활용하여 인증 확인

  • 인증 하기전.

  • 정상 인증 확인
    아래와 같이 docker login 명령어를 사용하여 Registry에 로그인할 수 있습니다.

docker login 192.168.0.31:5000

사용자명,를 입력. 로그인에 성공하면 Login Succeeded 메시지가 출력됨.

  • pull 성공.

✅ 단계 4: Docker 이미지 서명 및 무결성 검증

• Docker Content Trust (DCT) 활성화 및 서명된 이미지 푸시
• 서명되지 않은 이미지 업로드 시도 및 차단 여부 확인

Docker Content Trust(DCT)는 Docker 이미지의 서명과 무결성 검증을 통해, 이미지가 신뢰할 수 있는 출처에서 왔으며 변조되지 않았음을 보장하는 보안 기능. 4단계는 DCT를 활성화하고, 서명된 이미지를 push한 후 서명되지 않은 이미지가 업로드되지 않도록 하는 과정.

1. Docker Content Trust 활성화

  • 환경 변수 설정:
export DOCKER_CONTENT_TRUST=1
  • Notary 서버 사용:
    Docker Content Trust는 Notary라는 도구를 사용하여 서명 정보를 관리합니다. 이미지 push 시 Notary 서버와 통신하여 서명 데이터를 저장하고, pull 시 이를 검증합니다.

이 설정이 활성화되면, docker push, docker pull, docker run 등의 명령어는 이미지의 서명을 자동으로 확인하거나 서명하도록 동작합니다.

2. 서명된 이미지 푸시

이미지 서명 및 Push:
DCT 활성화 상태에서 이미지를 push하면, Docker CLI가 해당 이미지에 대해 서명 작업을 진행합니다.

docker push your-registry.example.com/your-image:tag

처음 push 시, 서명을 위한 키가 없다면 Docker CLI가 생성하라고 요청할 수 있음.
생성된 서명 정보는 Notary 서버에 저장되어, 이후 pull이나 run 시 이미지의 무결성을 확인하는 데 사용됨.

서명된 이미지의 장점:
서명된 이미지는 인증된 출처에서 왔으며, 내용이 변조되지 않았음을 보증. 이로 인해 배포 파이프라인에서 이미지 신뢰성을 강화할 수 있음.

3. notary docker-compose

docker hub를 사용할경우 notaryr가 안에 내장되있어서 저절로 처리가 되지만, private registry같은 경우는 안됨.

특히 쓰고있는 가상환경이 arm64인데 지원되는 버전도 없어서 docker를 사용할 수 밖에 없었음. 공식 홈페이지에 있는 notary docker-compose를 사용하여 서버 설치. https://github.com/notaryproject/notary

3. notary docker-compose 인증서 재발급.

문제는 안에 들어있는 인증서들이 만료되어서 모두 새로 다시 발급.

Crt 유효기간.

for crt in *.crt; do    echo "Checking $crt";   openssl x509 -in "$crt" -noout -dates;   echo "---------------------"; done

key와 crt 해시 검사.

for key in *.key; do   echo "Checking $key ...";   openssl x509 -noout -modulus -in "${key/.key/.crt}" | openssl md5;   openssl rsa -noout -modulus -in "$key" | openssl md5;   echo "------------------------------------"; done

인증서 제거

# Notary 인증서 및 키 삭제

rm -rf fixtures/notary-server.crt fixtures/notary-server.key fixtures/notary-server.csr

rm -rf fixtures/notary-signer.crt fixtures/notary-signer.key fixtures/notary-signer.csr

rm -rf fixtures/notary-escrow.crt fixtures/notary-escrow.key fixtures/notary-escrow.csr

# Root CA를 제외한 모든 인증서 삭제

rm -rf fixtures/secure.example.com.crt fixtures/self-signed_docker.com-notary.crt

rm -rf fixtures/self-signed_secure.example.com.crt

(1) Notary 서버 개인 키 생성

openssl genrsa -out fixtures/notary-server.key 4096
(2) CSR (Certificate Signing Request) 생성

openssl req -new -key fixtures/notary-server.key -out fixtures/notary-server.csr -subj "/CN=notary-server"
(3) Root CA로 Notary 서버 인증서 서명

openssl x509 -req -in fixtures/notary-server.csr -CA fixtures/root-ca.crt -CAkey fixtures/root-ca.key -CAcreateserial -out fixtures/notary-server.crt -days 365 -sha256

(1) Notary Signer 키 & CSR 생성

openssl genrsa -out fixtures/notary-signer.key 4096
openssl req -new -key fixtures/notary-signer.key -out fixtures/notary-signer.csr -subj "/CN=notary-signer"
openssl x509 -req -in fixtures/notary-signer.csr -CA fixtures/root-ca.crt -CAkey fixtures/root-ca.key -CAcreateserial -out fixtures/notary-signer.crt -days 365 -sha256
(2) Notary Escrow 키 & CSR 생성

openssl genrsa -out fixtures/notary-escrow.key 4096
openssl req -new -key fixtures/notary-escrow.key -out fixtures/notary-escrow.csr -subj "/CN=notary-escrow"
openssl x509 -req -in fixtures/notary-escrow.csr -CA fixtures/root-ca.crt -CAkey fixtures/root-ca.key -CAcreateserial -out fixtures/notary-escrow.crt -days 365 -sha256

키를 root키에 맞게 하나하나 다 맞춰주고 유효성도 확인 완료..!

특히 notary-server, notary-signer, db 세개의 컨테이너가 모두 각자의 컨테이너에서 접속이 되도록 해줘야하는데 인증서와 키를 모두 새로 만들었기때문에 맞춰주는것이 까다로웠음.

최종적으로 모두 상화작용이 되는것을 확인데도 docker regisry에 push pull이 도저히 안되서 포기. DCT는 그냥 docker hub를 사용할때 쓰자….!

✅ 단계 5: CI/CD 연동 및 자동화 테스트

• GitLab CI/CD와 Private Registry 연동
• .gitlab-ci.yml을 활용하여 CI/CD 파이프라인에서 자동으로 이미지 빌드 및 푸시

1. GitLab CI/CD 환경 변수 설정

새 repositroy 생성.

GitLab에서 Private Registry 인증을 위해 CI/CD 환경 변수를 설정.

📌 GitLab의 Repository → Settings → CI/CD → Variables에서 추가

변수명 값 (예시)
REGISTRY 해당 서버 IP:5000
REGISTRY_USER 사용자명 (htpasswd에서 설정한 값)
REGISTRY_PASSWORD 비밀번호 (htpasswd에서 설정한 값)

2. GitLab Runner 등록

3 .gitlab-ci.yml 작성

이제 GitLab CI/CD 파이프라인에서 Private Registry를 자동으로 사용하는 .gitlab-ci.yml 파일을 작성해야 함.

  • 로컬 머신에 repository clone

  • 📌 GitLab 프로젝트 루트 디렉토리에 .gitlab-ci.yml 파일 생성

stages:
  - build
  - push

variables:
  IMAGE_NAME: "$REGISTRY/my-app"  # 이미지 이름 지정 (my-app은 프로젝트명)
  DOCKER_DRIVER: overlay2  # GitLab Runner에서 도커 빌드할 때 필요

before_script:
  - echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin $REGISTRY  # Private Registry 로그인

build:
  stage: build
  script:
    - docker build -t $IMAGE_NAME:$CI_COMMIT_REF_NAME .  # 현재 브랜치명으로 태그
  only:
    - main  # main 브랜치에서만 실행

push:
  stage: push
  script:
    - docker push $IMAGE_NAME:$CI_COMMIT_REF_NAME  # 빌드된 이미지 푸시
  only:
    - main  # main 브랜치에서만 실행

4. GitLab CI/CD 실행 및 확인

이제 GitLab에 코드 (.gitlab-ci.yml 포함)를 푸시하면 자동으로 빌드 및 푸시가 진행됨.

git add .gitlab-ci.yml
git commit -m "Add GitLab CI/CD pipeline"
git push origin main

✅ error1

현재 발생한 docker: command not found -> GitLab Runner를 docker executor로 다시 설정

🔍 현재 발생한 문제 (shell executor의 문제점)
현재 GitLab Runner가 shell executor로 동작 중 → 실행 환경에 직접 docker가 설치되어 있어야 함.
하지만 현재 실행 환경에서 docker가 정상적으로 실행되지 않음 (docker: command not found, docker: unrecognized service 오류 발생).
따라서, Runner를 docker executor로 변경하면 GitLab CI/CD 파이프라인이 독립적인 컨테이너 내에서 실행되므로, 호스트 환경의 Docker 설치 여부에 영향을 받지 않게 됨.

✅ error2
tls: failed to verify certifictae

GitLab Runner 컨테이너에서 인증서를 신뢰하도록 설정해야 함.

1️⃣ 컨테이너 내부로 인증서 복사
호스트에서 GitLab Runner 컨테이너 내부로 인증서 복사

docker cp /home/evan/cert/private_registry.crt 761c05739e65:/usr/local/share/ca-certificates/192.168.0.31.crt

🔹 2️⃣ 컨테이너 내부에서 인증서 등록
GitLab Runner 컨테이너 내부에서 인증서 등록

컨테이너 내부로 들어간 후, 다음 명령어 실행:

update-ca-certificates

🔹 3️⃣ GitLab Runner 컨테이너 재시작
설정을 적용하기위해 GitLab Runner 컨테이너를 재시작

docker restart 761c05739e65

🔹 4️⃣ GitLab Runner 컨테이너 내부에서 Private Registry 로그인 테스트

docker exec -it 761c05739e65 /bin/sh
echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin 192.168.0.31:5000

Login Succeeded -> 성공.

✅ error3
ERROR: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

docker-compose.yml 수정 (GitLab Runner 컨테이너를 privileged 모드로 실행)

docker-compose down
docker-compose up -d
  • 아무리 권한을 바꿔주어도 컨테이너에 반영이 안되서 일단 임시로 sudo chmod 666 /var/run/docker.sock 로 바꿔주었다.

✅ 정상적으로 실행되면

GitLab CI/CD → Pipelines에서 실행 중인 빌드를 확인 가능
Private Registry (localhost:5000)에 새로 푸시된 이미지 확인 가능

curl -X GET https://localhost:5000/v2/_catalog
  • 성공...!

내가 gitlab에 push를 하면 이제 자동적으로 gitrunner에서 감지를 하고 이미지를 만들어서 나의 pirvate registry에 넣어준다...!
-dockerfile

# Nginx 공식 이미지 사용
FROM nginx:latest

# 빌드 타임에 환경 변수를 받아서 index.html 생성
ARG COMMIT_COUNT=0
RUN echo "<h1>Hello, GitLab CI/CD-${COMMIT_COUNT}!</h1>" > /usr/share/nginx/html/index.html

# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]
  • 최종 .gitlab-ci.yml 파일.
    커밋 횟수에 따라서 새로운 웹페이지를 만드는 이미지를 만들어준다.
  GNU nano 4.8                                                           .gitlab-ci.yml                                                                      
stages:
  - build
  - push


variables:
  IMAGE_NAME: "$REGISTRY/my-app"
  DOCKER_DRIVER: overlay2


before_script:
  - echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin $REGISTRY


build:
  stage: build
  script:
    - COMMIT_COUNT=$(git rev-list --count HEAD)
    - docker build --build-arg COMMIT_COUNT=$COMMIT_COUNT -t $IMAGE_NAME:$CI_COMMIT_REF_NAME .
  only:
    - main


push:
  stage: push
  script:
    - docker push $IMAGE_NAME:$CI_COMMIT_REF_NAME
  only:
    - main

이제 private registry에서 파일을 받아 컨테이너를 실행해보자.

docker run -d --name my-nginx -p 8080:80 192.168.0.31:5000/my-app:main

5. 최종 과제

  • dockerfile
# Nginx 공식 이미지 사용
FROM nginx:latest

# Git 커밋 수 & message.txt 내용을 받아서 index.html 생성
ARG COMMIT_COUNT=0
COPY message.txt /tmp/message.txt
RUN echo "<h1>$(cat /tmp/message.txt) - Version: ${COMMIT_COUNT}</h1>" > /usr/share/nginx/html/index.html

# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]
  • .gitlab-ci.yml
stages:
  - build
  - push

variables:
  IMAGE_NAME: "$REGISTRY/my-app"
  DOCKER_DRIVER: overlay2

before_script:
  - echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USER" --password-stdin $REGISTRY

build:
  stage: build
  script:
    - COMMIT_COUNT=$(git rev-list --count HEAD)
    - docker build --build-arg COMMIT_COUNT=$COMMIT_COUNT -t $IMAGE_NAME:$CI_COMMIT_REF_NAME .
  only:
    - main

push:
  stage: push
  script:
    - docker push $IMAGE_NAME:$CI_COMMIT_REF_NAME
  only:
    - main
  • variables(htpasswd)
  • message.txt

-> message.txt 수정.

-> gitlab에 push

-> 파이프라인 성공.
push받으면 gitlab-runner가 감지-> 도커 이미지 생성 -> private registry로 push

-> private registry에서 받은 이미지 pull

-> 기존 컨테이너 삭제후 방금 받은 새로운 이미지 run


message를 수정한대로 결과물이 잘나옴....!

✅ 후기

Docker Content Trust를 실패하기도 했고 시간을 너무 많이 써서 cd까지는 하지 못한것같다. 자동적으로 배포가 되야하는데 commit 후 gitlab runner를 통해 이미지를 private registry에 넣는것까지만 자동화고, 사실상 뒤에는 직접 다운받아서 컨테이너를 실행시켜야 동작이 된다.
쿠버네티스를 배우면 이것을 이용해서 더 그럴싸한 ci/cd환경을 만들고 싶다.

0개의 댓글