모바일에서만 회원가입이 안 됐던 이유 — 422 에러의 진짜 원인은 SSL이었다

— 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 개발일지
— 삽질도 기록이다

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다