이 코드는 관리자 비밀번호를 화면에 노출하지 않고 입력받은 뒤, 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의 -i는 interactive입니다.
여기서는 특히 중요합니다.
앞에서:
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 '...'는:
siteDocker 컨테이너 안에서 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에 넣기 위한 명령으로 보입니다.
가장 간단하게 한 문장으로 표현하면:
**"관리자 비밀번호를 안전하게 입력받아서
siteDocker 컨테이너의 Node.js + bcrypt로 cost 12 해싱한 뒤, 그 결과를 출력하고 입력한 비밀번호 변수는 삭제하는 Bash 스크립트"**입니다.
특히 초보자 입장에서 기억할 것은 이 6개입니다.
read → 비밀번호 입력
-s → 화면에 입력 내용 숨김
printf → 입력값 전달
| → 앞의 결과를 뒤로 전달
docker exec → 컨테이너 안에서 실행
bcrypt → 비밀번호 해시 생성
unset → 입력 비밀번호 변수 제거그리고 **12는 비밀번호 자체와 관계있는 숫자가 아니라 bcrypt의 계산 비용(cost factor)**입니다.
