이전 글목록 보기다음 글
docker2026-09-10T12:11:21.085Z

관리자 비밀번호 생성(bcrypt)

Anya2_Forger profileAnya2_Forger

이 코드는 관리자 비밀번호를 화면에 노출하지 않고 입력받은 뒤, Docker 컨테이너 안에서 bcrypt로 해시하여 출력하는 명령입니다.

쉽게 말하면:

비밀번호 입력 → Docker 컨테이너로 전달 → bcrypt로 암호화(정확히는 해싱) → 해시 출력 → 입력한 비밀번호 변수 삭제

입니다.

전체 코드를 한 줄씩 보겠습니다.


1. 전체 구조

read -rsp "관리자 비밀번호: " ADMIN_PASSWORD

printf '\n'

printf '%s' "$ADMIN_PASSWORD" |
docker exec -i site node -e '

let password = "";

process.stdin.setEncoding("utf8");

process.stdin.on("data", (chunk) => {

password += chunk;

});

process.stdin.on("end", async () => {

console.log(await require("bcrypt").hash(password, 12));

});

'

unset ADMIN_PASSWORD

실행하면 대략 이런 식입니다.

관리자 비밀번호:

여기에

mypassword123

를 입력해도 화면에는 mypassword123이 보이지 않습니다.

그리고 결과로:

$2b$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

같은 bcrypt 해시가 출력됩니다.


2. 첫 번째 줄

read -rsp "관리자 비밀번호: " ADMIN_PASSWORD

여기가 비밀번호를 입력받는 부분입니다.

read는 터미널에서 사용자 입력을 받는 Bash 명령입니다.

일반적인 경우라면:

read NAME

사용자가 입력한 값을 NAME 변수에 저장합니다.

예를 들어:

read NAME

그리고

pion79

라고 입력하면:

NAME="pion79"

와 비슷하게 저장됩니다.


그런데 여기에는 옵션이 있습니다.

-r
-s
-p

각각 의미가 있습니다.

-p

-p "관리자 비밀번호: "

입력 전에 메시지를 보여줍니다.

그래서:

관리자 비밀번호:

가 표시됩니다.


-s

가장 중요한 옵션입니다.

-s

입력한 내용을 화면에 표시하지 않습니다.

일반적으로:

read -p "비밀번호: " PASSWORD

라면

비밀번호: mypassword123

처럼 입력한 비밀번호가 화면에 나타날 수 있습니다.

하지만:

read -sp "비밀번호: " PASSWORD

라면

비밀번호:

만 나오고 입력한 문자는 표시되지 않습니다.


-r

-r

는 입력값에서 \ 같은 백슬래시를 특별하게 처리하지 않고 그대로 입력받도록 합니다.

비밀번호처럼 사용자가 입력한 문자열을 그대로 보존하고 싶을 때 넣어주는 것이 좋습니다.


마지막 부분

ADMIN_PASSWORD

이것은 입력받은 값을 저장할 변수 이름입니다.

즉:

read -rsp "관리자 비밀번호: " ADMIN_PASSWORD

를 쉽게 풀면:

"관리자 비밀번호를 화면에 표시하지 않고 입력받아서 ADMIN_PASSWORD라는 변수에 저장해라."

입니다.


3. 빈 줄 출력

printf '\n'

이건 단순합니다.

\n은 줄바꿈입니다.

즉:

printf '\n'

엔터 한 번 출력하는 것과 비슷합니다.

왜 필요할까요?

비밀번호를 입력할 때 read -s는 입력한 문자를 보여주지 않기 때문에 사용자가 Enter를 눌러도 화면상에서 줄바꿈이 자연스럽게 보이지 않을 수 있습니다.

그래서 입력이 끝난 뒤:

printf '\n'

으로 한 줄 내려주는 것입니다.


4. 핵심 부분

다음입니다.

printf '%s' "$ADMIN_PASSWORD" |
docker exec -i site node -e '
...
'

여기서 아주 중요한 것이 |입니다.

이 기호를 **파이프(pipe)**라고 합니다.

쉽게 생각하면:

프로그램 A의 출력
       ↓
      |
       ↓
프로그램 B의 입력

입니다.

즉:

printf '%s' "$ADMIN_PASSWORD"

가 비밀번호를 출력하고,

|

가 그 내용을 다음 명령으로 넘깁니다.

그리고 다음 명령이:

docker exec -i site node -e '...'

입니다.


5. printf '%s' "$ADMIN_PASSWORD"

printf '%s' "$ADMIN_PASSWORD"

여기서:

$ADMIN_PASSWORD

는 앞에서 입력한 비밀번호입니다.

예를 들어 사용자가:

Hello123!

를 입력했다면 내부적으로:

ADMIN_PASSWORD="Hello123!"

가 되어 있습니다.

따라서:

printf '%s' "$ADMIN_PASSWORD"

는 결과적으로:

Hello123!

를 출력합니다.

그런데 중요한 점이 있습니다.

printf '%s'

이므로 뒤에 줄바꿈을 붙이지 않습니다.

즉:

printf '%s' "abc"

는:

abc

이고,

printf '%s\n' "abc"

는:

abc

다음에 줄바꿈까지 들어갑니다.

비밀번호에서는 불필요한 줄바꿈이 bcrypt 입력값에 포함되지 않도록 '%s'를 사용한 것으로 볼 수 있습니다.


6. docker exec

다음 부분입니다.

docker exec -i site

이것은 Docker 컨테이너 안에서 명령을 실행하는 것입니다.

쉽게 말하면:

Ubuntu 서버
│
├── 현재 터미널
│
│
└── Docker
    │
    └── site 컨테이너

현재 Ubuntu 서버에서 입력한 명령이 Docker 컨테이너인:

site

안에서 실행됩니다.


-i는 무엇인가?

docker exec -i

-iinteractive입니다.

여기서는 특히 중요합니다.

앞에서:

printf '%s' "$ADMIN_PASSWORD" |

로 데이터를 파이프로 넘겼습니다.

그러면 Docker 컨테이너 안의 프로그램이 그 파이프에서 데이터를 읽어야 합니다.

그래서:

docker exec -i

를 사용합니다.

즉:

호스트
  │
  │ 비밀번호
  ↓
파이프
  │
  ↓
docker exec -i
  │
  ↓
site 컨테이너
  │
  ↓
Node.js

라는 흐름입니다.


7. site

docker exec -i site

여기서:

site

Docker 컨테이너 이름입니다.

현재 서버에서:

docker ps

를 실행하면 예를 들어:

CONTAINER ID   IMAGE        NAMES
abc123         site         site

같은 것이 나올 수 있습니다.

그러면:

docker exec -i site

site라는 실행 중인 컨테이너 안에서 명령을 실행하라.

라는 의미입니다.


8. node -e

다음입니다.

node -e '
...
'

node는 Node.js를 실행하는 프로그램입니다.

그리고:

-e

뒤에 작성한 JavaScript 코드를 바로 실행하라는 뜻입니다.

예를 들어:

node -e 'console.log("Hello")'

하면:

Hello

가 출력됩니다.

따라서:

docker exec -i site node -e '...'

는:

site Docker 컨테이너 안에서 Node.js를 실행하고, 뒤에 있는 JavaScript 코드를 실행하라.

는 뜻입니다.


9. JavaScript 코드 시작

이제 Docker 안에서 실행되는 JavaScript입니다.

let password = "";

password라는 변수를 만들고 빈 문자열로 시작합니다.

처음에는:

password = ""

입니다.


10. 표준 입력을 UTF-8로 처리

process.stdin.setEncoding("utf8");

여기서 process.stdin은 Node.js 프로그램이 입력받는 통로입니다.

현재 구조에서는:

printf
   ↓
파이프
   ↓
Docker
   ↓
Node.js process.stdin

입니다.

따라서 Node.js 입장에서는 stdin을 통해 비밀번호가 들어옵니다.

process.stdin.setEncoding("utf8");

는 들어오는 데이터를 UTF-8 문자열로 처리하라는 의미입니다.


11. data 이벤트

다음입니다.

process.stdin.on("data", (chunk) => {
    password += chunk;
});

처음 보면 조금 어렵지만 간단합니다.

Node.js에서 데이터가 들어오면 "data" 이벤트가 발생합니다.

예를 들어 비밀번호가:

Hello123

이라면 데이터가 들어옵니다.

chunk

라는 변수에 들어온 데이터 조각이 저장됩니다.

그리고:

password += chunk;

는:

password = password + chunk;

와 같습니다.

결국:

password
   ↓
"Hello123"

가 됩니다.


chunk라고 할까?

데이터가 항상 한 번에 전부 들어온다고 보장할 수 없기 때문입니다.

예를 들어:

Hello123

가 다음처럼 여러 조각으로 들어올 수도 있습니다.

chunk 1 → "Hel"
chunk 2 → "lo1"
chunk 3 → "23"

그러면:

password += chunk;

를 통해:

"Hel"
+ "lo1"
+ "23"

= "Hello123"

가 됩니다.

그래서 최종적으로 완전한 비밀번호를 구성합니다.


12. end 이벤트

다음 부분이 중요합니다.

process.stdin.on("end", async () => {

stdin으로 들어오는 데이터가 더 이상 없을 때 "end" 이벤트가 발생합니다.

즉:

비밀번호 입력 시작
       ↓
data
       ↓
data
       ↓
data
       ↓
입력 끝
       ↓
end

입니다.

따라서 이 코드는:

"비밀번호를 전부 받은 다음에 bcrypt 해시를 만들어라."

라는 구조입니다.


13. async

async () => {

여기서 async는 비동기 작업을 처리하기 위한 JavaScript 문법입니다.

bcrypt의 해시 생성은 시간이 걸리는 작업입니다.

그래서:

await

와 함께 사용합니다.


14. 가장 중요한 부분

console.log(await require("bcrypt").hash(password, 12));

이 한 줄이 실제로 비밀번호를 bcrypt로 해싱하는 부분입니다.

분해해 보겠습니다.


require("bcrypt")

require("bcrypt")

는 Node.js에서 bcrypt 패키지를 가져오는 것입니다.

즉:

bcrypt라는 라이브러리를 사용하겠다.

라는 의미입니다.

컨테이너 안에 bcrypt 패키지가 설치되어 있어야 합니다.


15. .hash(password, 12)

require("bcrypt").hash(password, 12)

bcrypt의 hash() 함수를 호출합니다.

첫 번째:

password

는 실제 비밀번호입니다.

두 번째:

12

는 bcrypt의 **cost factor(rounds)**입니다.

즉:

hash(password, 12)

는 대략:

이 비밀번호를 bcrypt cost 12로 해싱해라.

라는 뜻입니다.


16. 여기서 "암호화"와 "해싱"을 구분해야 합니다

엄밀하게 말하면 이것은:

❌ 암호화(encryption)

가 아니라

해싱(hashing)

입니다.

암호화는:

평문
 ↓ 암호화
암호문
 ↓ 복호화
평문

처럼 원래 값을 다시 얻을 수 있습니다.

하지만 bcrypt 해시는:

비밀번호
   ↓
bcrypt
   ↓
해시

이고 일반적으로 그 해시에서 원래 비밀번호를 복원하는 방식이 아닙니다.

예:

mypassword123
       ↓
bcrypt
       ↓
$2b$12$....................

따라서 관리자 비밀번호를 DB에 저장할 때는 일반적으로 이런 해시값을 저장합니다.


17. 12는 무엇인가?

.hash(password, 12)

12는 bcrypt의 작업 비용(cost)입니다.

쉽게 말하면:

"비밀번호를 해시하는 계산을 얼마나 어렵게 할 것인가?"

를 정하는 값입니다.

숫자가 높아질수록 계산량이 증가합니다.

예:

bcrypt(..., 10)
bcrypt(..., 12)
bcrypt(..., 14)

대략적으로 숫자가 높아질수록 더 많은 계산을 필요로 합니다.

따라서:

12

는 상당히 일반적으로 사용되는 bcrypt cost 값 중 하나입니다.


18. await

await require("bcrypt").hash(password, 12)

bcrypt가 해시를 만들 때까지 기다립니다.

예를 들어:

password
   ↓
bcrypt 계산 시작
   ↓
잠시 기다림
   ↓
해시 생성 완료
   ↓
다음 작업

이라는 의미입니다.


19. console.log

마지막으로:

console.log(...)

는 결과를 터미널에 출력합니다.

따라서 최종적으로:

$2b$12$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

같은 bcrypt 해시가 터미널에 표시됩니다.


20. 마지막 unset

마지막입니다.

unset ADMIN_PASSWORD

이것도 매우 중요합니다.

앞에서:

ADMIN_PASSWORD

변수에 비밀번호를 저장했습니다.

작업이 끝났으므로:

unset ADMIN_PASSWORD

를 실행해서 현재 셸에서 그 변수를 제거합니다.

즉:

작업 전

ADMIN_PASSWORD = "실제비밀번호"


        ↓ bcrypt 처리


작업 후

ADMIN_PASSWORD = 없음

이 됩니다.


21. 전체 흐름을 그림으로 보면

이 코드는 사실 다음 구조입니다.

                 Ubuntu 서버
                      │
                      │
             비밀번호 입력
                      │
                      ↓
        ┌────────────────────────┐
        │ ADMIN_PASSWORD 변수    │
        │ "실제 비밀번호"         │
        └───────────┬────────────┘
                    │
                    │ printf
                    ↓
                  pipe |
                    │
                    ↓
        ┌────────────────────────┐
        │ Docker site      │
        │                        │
        │       Node.js          │
        │          │             │
        │          ↓             │
        │       stdin            │
        │          │             │
        │          ↓             │
        │       password         │
        │          │             │
        │          ↓             │
        │       bcrypt           │
        │       cost 12          │
        └──────────┬─────────────┘
                   │
                   ↓
             bcrypt 해시
                   │
                   ↓
              터미널 출력
                   │
                   ↓
          ADMIN_PASSWORD 삭제

22. 이 코드의 핵심 장점

이 코드는 특히 다음 부분을 잘 처리하고 있습니다.

① 비밀번호를 화면에 표시하지 않음

read -rsp

때문입니다.


② 비밀번호를 명령줄 인자로 직접 넣지 않음

예를 들어 이런 방식은 피하는 것이 좋습니다.

node -e 'bcrypt.hash("mypassword123", 12)'

비밀번호가 명령 자체에 들어가 버립니다.

현재 코드는:

printf '%s' "$ADMIN_PASSWORD" | docker exec -i ...

형태로 **표준 입력(stdin)**을 통해 전달합니다.


③ Docker 컨테이너 안에서 bcrypt를 실행

docker exec -i site node -e ...

이므로 호스트 Ubuntu에 bcrypt가 설치되어 있을 필요 없이, site 컨테이너의 Node.js 환경에 bcrypt가 설치되어 있으면 됩니다.


④ 마지막에 비밀번호 변수 삭제

unset ADMIN_PASSWORD

로 작업이 끝난 뒤 셸 변수에서 제거합니다.


23. 이 코드에서 특히 알아두어야 할 점

한 가지 중요한 부분이 있습니다.

unset ADMIN_PASSWORD

변수에서 삭제하는 것이지, "비밀번호가 컴퓨터 메모리에서 완전히 0으로 덮어써졌다"는 의미는 아닙니다.

또한 bcrypt 결과는 화면에 출력됩니다.

예:

$2b$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

따라서 이 해시를 복사해서 관리자 계정의 비밀번호 해시가 필요한 설정/DB 등에 저장하는 용도로 사용하는 것입니다.


24. 결국 이 명령의 목적

아마 현재 상황에서는 Docker로 운영되는 웹사이트의 관리자 비밀번호를 bcrypt 해시로 만들어 설정하거나 DB에 넣기 위한 명령으로 보입니다.

가장 간단하게 한 문장으로 표현하면:

**"관리자 비밀번호를 안전하게 입력받아서 site Docker 컨테이너의 Node.js + bcrypt로 cost 12 해싱한 뒤, 그 결과를 출력하고 입력한 비밀번호 변수는 삭제하는 Bash 스크립트"**입니다.

특히 초보자 입장에서 기억할 것은 이 6개입니다.

read       → 비밀번호 입력
-s         → 화면에 입력 내용 숨김
printf     → 입력값 전달
|          → 앞의 결과를 뒤로 전달
docker exec → 컨테이너 안에서 실행
bcrypt     → 비밀번호 해시 생성
unset      → 입력 비밀번호 변수 제거

그리고 **12는 비밀번호 자체와 관계있는 숫자가 아니라 bcrypt의 계산 비용(cost factor)**입니다.

Comments

Log in to comment

Loading comments...
이전 글목록 보기다음 글

당신의 이야기를 기다리고 있습니다