보뇨 다이어리

사설 인증서 로컬 신뢰 처리 본문

컴퓨터 관련/환경 정보

사설 인증서 로컬 신뢰 처리

보뇨 2026. 9. 2. 09:18
반응형

사설 인증서 로컬 신뢰 처리 (macOS)

Chrome에서 NET::ERR_CERT_AUTHORITY_INVALID 가 뜨는 내부 클러스터 도메인(*.idc1.ten1010.io 등)을 로컬에서 신뢰시키는 절차. 클러스터 인증서가 재발급될 때마다 반복한다.

아래에서 HOST 는 대상 도메인(예: aipub.cluster3.idc1.ten1010.io)이다.

export HOST=example.com
export KC=~/Library/Keychains/login.keychain-db

Phase 1. 서버가 주는 인증서 확인

echo | openssl s_client -connect $HOST:443 -servername $HOST 2>/dev/null \
  | openssl x509 -noout -subject -issuer -fingerprint -sha256 -dates

확인할 것:

  • subject: 보통 CN=*.clusterN.idc1.ten1010.io (와일드카드)
  • issuer: CN=*.idc1.ten1010.io (사설 CA, 서버가 체인에 안 실어줌)
  • notBefore: 최근 날짜면 재발급된 것

Phase 2. Keychain에 이미 있는지 비교

security find-certificate -a -c "$(echo | openssl s_client -connect $HOST:443 -servername $HOST 2>/dev/null \
  | openssl x509 -noout -subject | sed 's/^subject=CN *= *//')" -p $KC \
  | openssl x509 -noout -fingerprint -sha256 -dates
  • 결과 없음 → 처음 신뢰하는 클러스터. Phase 3만 실행.
  • 결과 있고 fingerprint가 Phase 1과 다름 → 재발급된 것. Phase 3에서 옛것 삭제 후 추가.
  • fingerprint 같음 → 인증서 문제 아님. Chrome 재시작 / HSTS 캐시(Phase 5) 확인.

Phase 3. 인증서 교체 + 신뢰

# 서버 leaf 저장
echo | openssl s_client -connect $HOST:443 -servername $HOST 2>/dev/null \
  | openssl x509 -outform PEM > /tmp/$HOST.pem

# (재발급인 경우만) 옛 인증서 삭제 — Phase 2에서 나온 fingerprint의 콜론 제거해서 넣는다
security delete-certificate -Z <OLD_SHA256_NO_COLON> $KC

# 새 인증서 추가 + 루트로 신뢰 (로그인 keychain, sudo 불필요, GUI 인증창 뜰 수 있음)
security add-trusted-cert -r trustAsRoot -k $KC /tmp/$HOST.pem

Phase 4. 검증

curl -sS -o /dev/null -w "HTTP %{http_code}  ssl_verify=%{ssl_verify_result}\n" https://$HOST/

ssl_verify=0 이면 성공. 0이 아니면 Phase 1의 fingerprint와 keychain 것이 같은지 다시 확인.

Phase 5. Chrome 반영

  1. Chrome 완전 종료 후 재시작 (keychain 신뢰 캐시).
  2. 그래도 경고 나오면 chrome://net-internals/#hsts → Delete domain security policies 에 HOST 입력 → Delete.
  3. 접속 재시도.

자주 겪는 상황

증상원인조치

잘 되던 도메인이 갑자기 ERR_CERT_AUTHORITY_INVALID 클러스터 cert 재발급 Phase 1~4 (옛것 삭제 포함)
새 클러스터 처음 접속 keychain에 없음 Phase 1, 3, 4
curl은 OK인데 Chrome만 경고 Chrome 캐시 / HSTS Phase 5
add-trusted-cert 권한 오류 System keychain 지정함 $KC 가 login.keychain-db 인지 확인

근본 해결

leaf를 클러스터별로 신뢰하는 방식은 재발급마다 반복된다. 인프라 팀에서 CA 인증서(CN=*.idc1.ten1010.io)를 받아 그것 하나만 trustAsRoot 로 넣으면 하위 클러스터 cert가 바뀌어도 다시 할 필요 없다.

 

사실 하다가 그냥 쉘스크립트로 만들면 좋겠다 싶어서 아래와 같이 만듦.

#!/usr/bin/env bash
# Trust a private-CA-signed TLS certificate for a host in the macOS login keychain.
#
# Usage:
#   scripts/trust-cert.sh <host>[:port]
#   scripts/trust-cert.sh https://aipub.cluster3.idc1.ten1010.io/
#
# Steps: fetch leaf cert -> compare with keychain -> replace stale copy -> trust as root -> verify with curl.
set -euo pipefail

KEYCHAIN="${KEYCHAIN:-$HOME/Library/Keychains/login.keychain-db}"

usage() {
  echo "usage: $0 <host>[:port] | <https://url>" >&2
  exit 2
}

log()  { printf '\033[1;34m[%s]\033[0m %s\n' "$1" "$2"; }
ok()   { printf '\033[1;32m  ✔\033[0m %s\n' "$1"; }
warn() { printf '\033[1;33m  !\033[0m %s\n' "$1"; }
die()  { printf '\033[1;31m  ✘\033[0m %s\n' "$1" >&2; exit 1; }

[[ $# -eq 1 ]] || usage
[[ "$(uname)" == "Darwin" ]] || die "macOS only (uses security(1))."

# --- normalize input: strip scheme / path, split host:port ---------------------
input="${1#*://}"
input="${input%%/*}"
HOST="${input%%:*}"
PORT="${input##*:}"
[[ "$PORT" == "$HOST" ]] && PORT=443
[[ -n "$HOST" ]] || usage

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
LEAF="$TMP/leaf.pem"

# --- Phase 1: fetch served certificate ---------------------------------------
log "1/5" "Fetching certificate from $HOST:$PORT"
if ! echo | openssl s_client -connect "$HOST:$PORT" -servername "$HOST" 2>/dev/null \
     | openssl x509 -outform PEM > "$LEAF" 2>/dev/null || [[ ! -s "$LEAF" ]]; then
  die "Could not fetch certificate from $HOST:$PORT"
fi

SUBJECT_CN="$(openssl x509 -in "$LEAF" -noout -subject | sed -E 's/^subject=.*CN *= *//; s/,.*$//')"
ISSUER="$(openssl x509 -in "$LEAF" -noout -issuer | sed -E 's/^issuer=//')"
NEW_FP="$(openssl x509 -in "$LEAF" -noout -fingerprint -sha256 | sed 's/.*=//; s/://g')"
NOT_BEFORE="$(openssl x509 -in "$LEAF" -noout -startdate | sed 's/.*=//')"
NOT_AFTER="$(openssl x509 -in "$LEAF" -noout -enddate | sed 's/.*=//')"

ok "subject CN : $SUBJECT_CN"
ok "issuer     : $ISSUER"
ok "sha256     : $NEW_FP"
ok "valid      : $NOT_BEFORE  ->  $NOT_AFTER"

# --- Phase 2: compare with keychain ------------------------------------------
log "2/5" "Checking login keychain for existing '$SUBJECT_CN'"
EXISTING_FPS="$(security find-certificate -a -c "$SUBJECT_CN" -Z "$KEYCHAIN" 2>/dev/null \
  | awk '/SHA-256 hash:/ {print $3}' || true)"

ALREADY_TRUSTED=0
STALE_FPS=()
if [[ -z "$EXISTING_FPS" ]]; then
  ok "not present -> first-time trust"
else
  while IFS= read -r fp; do
    [[ -z "$fp" ]] && continue
    if [[ "$fp" == "$NEW_FP" ]]; then
      ALREADY_TRUSTED=1
      ok "identical certificate already in keychain"
    else
      STALE_FPS+=("$fp")
      warn "stale certificate found: $fp"
    fi
  done <<< "$EXISTING_FPS"
fi

# --- Phase 3: replace + trust --------------------------------------------------
log "3/5" "Updating keychain"
for fp in "${STALE_FPS[@]:-}"; do
  [[ -z "$fp" ]] && continue
  security delete-certificate -Z "$fp" "$KEYCHAIN"
  ok "removed stale $fp"
done

if [[ $ALREADY_TRUSTED -eq 1 ]]; then
  ok "skip add (already present); re-applying trust setting"
fi
# add-trusted-cert is idempotent on the same cert; may prompt a GUI auth dialog.
security add-trusted-cert -r trustAsRoot -k "$KEYCHAIN" "$LEAF"
ok "trusted as root in $(basename "$KEYCHAIN")"

# --- Phase 4: verify -----------------------------------------------------------
log "4/5" "Verifying with curl"
RESULT="$(curl -sS -o /dev/null -w '%{http_code} %{ssl_verify_result}' --max-time 15 "https://$HOST:$PORT/" 2>&1 || true)"
HTTP_CODE="${RESULT%% *}"
SSL_VERIFY="${RESULT##* }"
if [[ "$SSL_VERIFY" == "0" ]]; then
  ok "HTTP $HTTP_CODE, ssl_verify=0"
else
  die "TLS verification still failing: $RESULT"
fi

# --- Phase 5: browser hints ----------------------------------------------------
log "5/5" "Done. Browser steps:"
echo "    1. Fully quit and restart Chrome (keychain trust is cached)."
echo "    2. If the warning persists (HSTS): chrome://net-internals/#hsts"
echo "       -> 'Delete domain security policies' -> enter '$HOST' -> Delete."
echo
echo "    Note: issuer is '$ISSUER'. Trusting that CA once would avoid repeating this per cluster."
반응형