— 422 에러의 진짜 원인은 SSL이었다
증상은 단순했다
PC에서는 회원가입이 잘 됐다.
그런데 모바일에서 가입 버튼을 누르면 이런 메시지가 떴다.
422 Unprocessable Entity
"The change you wanted was rejected."
같은 서버, 같은 코드, 같은 폼.
왜 모바일에서만?
처음엔 흔한 용의자들을 의심했다
Rails에서 422 에러가 나면 보통 이런 것들을 먼저 본다.
- CSRF 토큰 문제?
- Turbo가 뭔가 꼬였나?
- 폼 파라미터가 잘못됐나?
하나씩 확인했다.
<%# Turbo 비활성화 - 이미 적용되어 있었다 %>
<%= form_for(resource, html: { data: { turbo: false } }) do |f| %>
<%# csrf_meta_tags - 레이아웃에 정상적으로 있었다 %>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
# production.rb - SSL 설정도 되어 있었다
config.force_ssl = true
config.assume_ssl = true
다 정상이었다.
그런데 왜 모바일에서만 실패하는 걸까?
로그는 거짓말을 안 한다
서버 로그를 열어봤다.
Devise::Controllers::Helpers#handle_unverified_request
verify_authenticity_token
CSRF 토큰 검증 실패.
Rails가 “너 진짜 사람이냐?”고 문 앞에서 막은 거다.
그런데 이상했다.
CSRF 토큰은 분명히 폼에 들어가 있었다.
csrf_meta_tags도 있었다.
그렇다면 토큰이 전송되지 않았거나, 매칭이 안 됐다는 뜻이다.
퍼즐이 맞춰지기 시작했다
CSRF 토큰은 세션을 기반으로 동작한다.
서버가 세션에 토큰을 저장하고, 폼에서 보낸 토큰과 비교한다.
그런데 세션은 쿠키로 유지된다.
여기서 힌트를 얻었다.
# production.rb
config.force_ssl = true
이 설정이 켜져 있으면 Rails는 자신이 HTTPS 환경이라고 믿는다.
그래서 세션 쿠키에 Secure 플래그를 붙인다.
Secure 쿠키는 HTTPS 연결에서만 전송된다.
그런데 내 서버는?
실제로는 SSL 인증서가 없었다.
문제의 본질
상황을 정리하면 이랬다.
Rails: "나는 HTTPS 환경이야" (force_ssl = true)
↓
쿠키: Secure 플래그가 붙음
↓
실제 서버: SSL 인증서 없음, HTTP로 접속됨
↓
브라우저: "Secure 쿠키는 HTTP에서 안 보내"
↓
서버: 세션 쿠키가 안 옴 → 세션 없음
↓
CSRF 토큰 매칭 실패
↓
💥 422 Unprocessable Entity
PC에서는 왜 됐을까?
브라우저마다, 그리고 같은 브라우저라도 PC와 모바일에서 쿠키 정책이 다르다.
모바일 브라우저가 더 엄격하게 Secure 플래그를 체크한 것이다.
해결: SSL을 제대로 붙이자
Kamal 2.x에서 Let’s Encrypt SSL 설정은 간단했다.
config/deploy.yml:
# Before
proxy:
app_port: 3000
healthcheck:
path: /up
# After
proxy:
ssl: true
host: healthnote.space
app_port: 3000
healthcheck:
path: /up
딱 두 줄 추가.
ssl: true
host: healthnote.space
그리고 servers.web.hosts는 도메인이 아니라 IP 주소로 변경했다.
(이건 SSH 접속용이지, SSL 설정과는 별개다)
servers:
web:
hosts:
- 13.125.176.249 # 도메인 대신 IP
배포 전 체크: 방화벽
SSL 인증서 발급을 위해 Let’s Encrypt가 80번 포트로 검증 요청을 보낸다.
443번 포트도 열려 있어야 HTTPS 트래픽을 받을 수 있다.
AWS Lightsail 콘솔에서 확인했다.
| Application | Protocol | Port |
|---|---|---|
| SSH | TCP | 22 |
| HTTP | TCP | 80 |
| HTTPS | TCP | 443 |
443이 없었다면 추가해야 했을 것이다.
다행히 이미 열려 있었다.
배포
git add config/deploy.yml
git commit -m "Enable SSL with Let's Encrypt"
kamal deploy
배포 후 브라우저에서 확인했다.
주소창의 “주의 요함” 경고가 사라지고, 자물쇠 아이콘이 떴다.
모바일에서 다시 테스트
회원가입 버튼을 눌렀다.
422 에러 없이 가입 성공.
돌아보며
이 문제를 처음 만났을 때, 나는 코드부터 뒤졌다.
Turbo 설정, CSRF 토큰, 폼 파라미터…
하지만 진짜 원인은 코드 밖에 있었다.
Rails는 자신이 HTTPS 환경이라고 믿고 있었고,
실제 인프라는 그 믿음을 뒷받침하지 못하고 있었다.
그 간극에서 모바일 브라우저가 먼저 문제를 감지한 것이다.
오늘의 교훈
설정과 현실이 일치하는지 확인하라.
force_ssl = true라고 썼으면, 진짜 SSL이 있어야 한다.
코드는 거짓말을 하지 않지만, 설정은 때때로 희망사항이 된다.
그리고 하나 더.
모바일에서만 안 되면, 쿠키 정책을 의심하라.
PC와 모바일은 같은 브라우저라도 보안 정책이 다를 수 있다.
특히 Secure, SameSite 같은 쿠키 속성에서.
정리
| 항목 | 내용 |
|---|---|
| 증상 | 모바일에서만 회원가입 422 에러 |
| 원인 | SSL 없이 force_ssl = true 설정 → Secure 쿠키 미전송 → 세션 없음 → CSRF 실패 |
| 해결 | Kamal에서 Let’s Encrypt SSL 활성화 |
| 교훈 | 설정과 현실의 간극을 확인하라 |
이 글이 같은 문제로 헤매는 누군가에게 도움이 되길 바란다.
Senior HealthNote 개발일지
— 삽질도 기록이다
