
Rails의 암호화와 해시: master.key와 Devise가 내부에서 수행하는 일
요약
Rails에서 사용하는 암호화(Encryption)와 해시(Hashing)의 차이점을 설명합니다. master.key를 이용한 AES-256-GCM 방식의 credentials 관리와 Devise를 통한 비밀번호 해싱의 내부 동작 원리를 다룹니다.
핵심 포인트
- 암호화는 키를 통해 복원이 가능한 가역적 과정이며, 해시는 복원이 불가능한 불가역적 과정임
- Rails credentials는 AES-256-GCM 대칭 암호를 사용하여 데이터의 기밀성과 무결성을 보장함
- master.key 유출 시 모든 기밀 정보가 노출되므로 환경 변수(RAILS_MASTER_KEY) 관리가 필수적임
- GCM 모드와 IV(초기화 벡터)를 통해 동일한 평문이라도 매번 다른 암호문을 생성하여 보안성을 높임
이 기사의 요점
master.key는 AES-256-GCM으로 암호화된 credentials.yml.enc를 복호화하기 위한 대칭키이며, 분실하면 credentials는 영구적으로 읽을 수 없게 됩니다. - Devise는 비밀번호를 해시(Hashing, 불가역 변환)로 저장하기 때문에, 원칙적으로 "원래 비밀번호로 되돌리는 것"은 불가능합니다.
- 암호화(Encryption, 가역)와 해시화(Hashing, 불가역)는 목적이 다릅니다. Rails는 유스케이스에 따라 두 가지를 구분하여 사용하고 있습니다.
Rails를 사용하다 보면 "암호화"와 "해시화"라는 용어가 혼재되어 등장합니다. 둘 다 "평문을 그대로 저장하지 않는다"는 점은 공통적이지만, 근본적인 목적이 다릅니다.
| 작업 | 방향성 | 대표 사례 |
|---|---|---|
| 암호화 (Encryption) | 가역 (키가 있으면 복원 가능) | credentials.yml.enc, ActiveRecord::Encryption |
| 해시화 (Hashing) | 불가역 (원래대로 되돌릴 수 없음) | Devise의 비밀번호 저장 |
"왜 비밀번호를 해시로 만드는가", "왜 API 키는 암호화인가" —— 이 구분을 이해하면 Rails의 보안 설계가 하나의 맥락으로 보입니다.
Rails 5.2 이후, 기밀 정보 관리에는 Credentials라는 메커니즘이 사용되고 있습니다. config/credentials.yml.enc가 리포지토리에 포함되어 있으며, config/master.key는 .gitignore에 추가하여 관리합니다.
# 암호화된 파일을 편집하기
rails credentials:edit
이 명령을 실행하면 master.key를 사용하여 일시적으로 복호화한 YAML 파일을 에디터로 열고, 저장 시 재암호화합니다.
Rails가 사용하는 것은 AES-256-GCM (Advanced Encryption Standard, 256비트 키, GCM 모드)입니다. 이는 대칭 암호(Symmetric Encryption)의 일종으로, 암호화와 복호화에 동일한 키(master.key)를 사용합니다.
GCM (Galois/Counter Mode)은 스트림 암호 모드의 일종으로, 인증 태그를 부여함으로써 변조 탐지도 수행합니다. 단순히 "비밀로 유지하는 것"뿐만 아니라 "내용이 수정되지 않았음을 보장하는" 특성도 가지고 있습니다.
master.key의 내용은 32바이트의 랜덤한 16진수 문자열(예: a3f2c1...)입니다. 이것이 유출되면 credentials.yml.enc의 모든 내용을 읽을 수 있게 되므로, 운영 환경에서는 환경 변수 RAILS_MASTER_KEY로 주입하는 것이 일반적입니다.
Rails가 credentials의 암호화에 내부적으로 사용하고 있는 것은 ActiveSupport::MessageEncryptor입니다. 콘솔에서 실제로 다뤄볼 수 있습니다.
require "active_support/message_encryptor"
key = ActiveSupport::KeyGenerator.new("passphrase").generate_key("salt", 32)
encryptor = ActiveSupport::MessageEncryptor.new(key)
...
encrypt_and_sign은 암호화와 서명을 동시에 수행하며, decrypt_and_verify는 서명 검증 후에 복호화합니다. 변조된 암호문을 전달하면 ActiveSupport::MessageVerifier::InvalidSignature가 발생하므로 안전하게 사용할 수 있습니다.
실제로 콘솔에서 테스트해 보면, 동일한 평문이라도 호출할 때마다 다른 암호문이 생성되는 것을 확인할 수 있습니다. GCM 모드에서는 암호화할 때마다 **IV (초기화 벡터, Initialization Vector)**가 랜덤하게 생성되기 때문입니다. 이를 통해 동일한 값이 암호화되더라도 암호문을 추측할 수 없습니다.
Devise는 기본적으로 비밀번호를 bcrypt로 해시화하여 저장합니다 (bcrypt-ruby gem). bcrypt는 암호학적 해시 함수가 아니라 비밀번호 해시 전용 알고리즘이며, 다음과 같은 특성을 가집니다.
- 단방향성 (One-wayness): 해시 값으로부터 원래의 비밀번호를 계산하는 것은 (현실적으로) 불가능합니다.
- 솔트 (Salt): 동일한 비밀번호라도 매번 다른 해시가 생성됩니다. 이는 레인보우 테이블 (Rainbow Table) 공격을 방지합니다.
- 스트레칭 (Stretching, 비용 파라미터): 계산을 의도적으로 느리게 만들어 브루트 포스 (Brute-force) 공격을 어렵게 만듭니다.
Devise의 기본 비용(cost)은 11입니다 (Devise.stretches로 변경 가능). 비용 11에서는 2^11 = 2048회의 내부 반복이 수행됩니다.
users 테이블의 encrypted_password 컬럼에는 다음과 같은 문자열이 들어 있습니다.
$2a$11$hKUL...(60자)
이 문자열은 다음과 같은 부분으로 나뉩니다.
$2a$ - bcrypt의 버전 식별자
$11$ - 비용 파라미터 (= 2^11회 반복)
hKUL..(22자) - 솔트 (자동 생성·랜덤)
...
솔트는 해시 안에 함께 포함되어 있으므로, 검증 시 별도의 테이블에서 관리할 필요가 없습니다.
사용자가 로그인 폼에 비밀번호를 입력하면, Devise는 다음과 같은 작업을 수행합니다.
# Devise가 내부에서 수행하는 작업 (개념적인 의사 코드)
def valid_password?(plain_password)
bcrypt_hash = BCrypt::Password.new(self.encrypted_password)
...
end
중요한 점은 "저장된 해시와 입력값의 해시를 비교하고 있다"는 것입니다. 평문으로 되돌리는 작업은 전혀 수행되지 않습니다.
bcrypt-ruby gem을 직접 사용하면 이 동작을 확인할 수 있습니다.
require "bcrypt"
hash = BCrypt::Password.create("my_password", cost: 11)
# => "$2a$11$"...
답은 간단합니다. "애플리케이션이 원래의 비밀번호를 알 필요가 없기" 때문입니다. 로그인 검증에는 "동일한 값인지 확인만 할 수 있으면 충분"하며, 만약 복원이 가능하다면 DB 유출 시 피해가 매우 커집니다.
반면 API 키나 외부 서비스의 토큰은 Rails에서 외부 API로 전송해야 하므로, 복호화할 수 없다면 사용할 수 없습니다. 그래서 credentials (암호화)로 관리합니다.
Rails 7.0에서 도입된 ActiveRecord::Encryption을 사용하면 모델 속성을 투명하게 암호화할 수 있습니다.
class User < ApplicationRecord
encrypts :phone_number
end
이 기능은 내부적으로 ActiveSupport::MessageEncryptor와 동일한 메커니즘을 사용하며, 키 관리도 credentials를 통해 수행합니다. 개인정보나 기밀 속성을 저장할 때 유용합니다 (Rails Guides: Active Record Encryption).
Rails의 기본 세션 스토어 (Cookie Store) 역시 secret_key_base (credentials에 저장)를 사용한 서명 및 암호화가 이루어집니다. 세션 데이터는 암호화된 상태로 클라이언트의 Cookie에 저장되지만, secret_key_base가 변경되면 모든 기존 세션이 무효화됩니다. 이 또한 "가역적인 암호화 (Reversible Encryption)"의 한 예입니다.
Rails가 제공하는 암호화 및 해시 기능을 정리하면 다음과 같습니다.
| 기능 | 방식 | 알고리즘 | 목적 |
|---|---|---|---|
| credentials.yml.enc | 암호화 (가역) | AES-256-GCM | API 키 등 기밀 정보 저장 |
| ... |
"나중에 다시 꺼내야 하는가"라는 질문이 암호화와 해시 중 무엇을 선택할지 결정하는 판단 기준이 됩니다. 프레임워크를 사용하는 것에 그치지 않고 내부 메커니즘을 이해해 두면, 보안 설계 리뷰나 인시던트 대응 시 길을 잃지 않을 것입니다.
Q. master.key를 분실하면 credentials.yml.enc의 내용을 꺼낼 수 있나요?
꺼낼 수 없습니다. AES-256-GCM은 대칭 암호 (Symmetric Encryption) 방식이므로, 키 없이는 복호화가 현실적으로 불가능합니다. 운영 환경에서는 RAILS_MASTER_KEY
를 환경 변수로서 안전한 장소(AWS Secrets Manager 등)에 보관하고, master.key 본체의 백업도 별도로 확보해 두는 것이 권장됩니다.
Q. Devise의 비밀번호를 「리셋」할 때는 어떻게 하나요?
「복호화 (Decryption)」는 수행하지 않습니다. 비밀번호 리셋 시에는 일시적으로 유효한 토큰을 이메일로 보내고, 해당 링크를 통해 새로운 비밀번호를 재설정하게 하는 흐름입니다. 토큰 자체도 데이터베이스에는 digest (해시 (Hash))로 저장되어 있으며, Devise::TokenGenerator를 통해 검증됩니다.
Q. bcrypt의 비용 파라미터 (Cost Parameter)를 높이면 어떻게 되나요?
비용을 1 늘리면 계산 시간이 약 2배가 됩니다. 비용 11(기본값) → 비용 12로 설정하면, 비밀번호 해시 (Password Hash) 계산이 2배 느려지는 대신 무차별 대입 공격 (Brute-force attack)도 2배 더 어려워집니다. 다만 로그인 처리의 응답 시간 (Response time)에도 영향을 미치므로, 서버 사양과 공격 내성 사이의 균형을 고려하여 결정합니다. 운영 환경의 실측값을 바탕으로 조정하는 것이 현실적인 접근 방식입니다 (Devise.stretches로 변경 가능).
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기