Deployment Runbook — New Site

ติดตั้ง MFA (TOTP) บน ClearPass Guest Captive Portal

คู่มือทำมือ ทีละขั้น สำหรับ deploy ระบบ 2FA ที่ผ่านการทดสอบจริงแล้วจาก pilot site — ครอบคลุมทั้ง 3 ระบบที่ต้องแตะ พร้อม gotcha ทุกจุดที่เจอมาแล้ว จะได้ไม่ต้องไปเจอซ้ำที่ site ใหม่

3
ระบบที่ต้อง config
4
Part หลัก
9
Gotcha ที่บันทึกไว้
~2
ชม. ต่อ site (โดยประมาณ)
STEP 1 · PASSWORD
ClearPass Guest
Pre-Auth RADIUS →
IDENTITY CHECK
[Local User Repository]
STEP 2 · OTP
Aruba Controller
CONTROLLER-INITIATED
RADIUS Access-Request
Proxy →
OTP ORACLE
FreeRADIUS + PAM
SEED STORE
/etc/freeradius/ga/*
01

FreeRADIUS + Enrollment VM

VM เดียว ทำหน้าที่ 2 อย่าง: (ก) OTP oracle — เก็บ seed file แล้วตรวจรหัส TOTP ผ่าน PAM (ข) เว็บ self-service enrollment ให้ user สแกน QR ตั้งค่าเองได้ — ไม่ต้องมี real Linux account หรือรหัสผ่านของใครเก็บอยู่ในนี้เลย

Automated (site-specific only): ขั้น 1.1–1.5 (Clone VM golden image แล้ว) ทำแทนได้ด้วย site-setup.sh — รันครั้งเดียว ใส่แค่ ClearPass IP, proxy secret, enroll secret, IP ของ VM เอง ก็จบ ทดสอบ idempotent แล้วบน site จริง (รันซ้ำ 3 รอบ ผลลัพธ์เหมือนเดิมทุกครั้ง)
Automated (from a blank VM), live-tested end-to-end — both halves: install-mfa-host.sh ทำขั้น 1.1–1.6 ทั้งหมดจากเครื่องเปล่า (apt install, PAM binding, systemd override, mfaenroll user + sudoers, deploy app.py + venv, systemd unit) แล้วต่อท้ายด้วย site-setup.sh logic เดิมในตัวเดียวกัน — ทดสอบครบ 2 รอบบนเครื่อง pilot ที่มี demo ทำงานอยู่จริง: รอบแรกส่วนติดตั้งทั่วไป (--skip-site-config) รอบสองส่วน site-specific จริง โดยดึงค่า secret ที่ตั้งไว้แล้วบนเครื่องมาป้อนกลับเข้าไปเอง (ไม่ใช่ค่าที่เดา) แล้วให้ script restart ทั้ง freeradius และ mfa-enroll จริง — ทุกขั้นออก [ok] ครบ, healthz ตอบ 200, radtest reject รหัสผิดถูกต้อง, clients.conf มี block client clearpass แค่ตัวเดียวไม่ซ้ำ ไม่กระทบ demo ที่ทำงานอยู่ก่อนเลย
📝
ตอนทดสอบเจอ 2 จุดที่ต่างจาก config เดิมของเครื่อง pilot (ทั้งคู่ไม่กระทบอะไร ทดสอบแล้วทำงานถูกต้อง): (1) sudoers rule เดิมชื่อไฟล์ /etc/sudoers.d/mfaenroll ตอนแรกสคริปต์เขียนเป็นคนละชื่อ (mfaenroll-chown) — เนื้อหากฎเหมือนกันเป๊ะ แก้ให้ตรงชื่อเดิมแล้วหลังทดสอบ (2) deployment เดิมรัน Flask ด้วย system /usr/bin/python3 ตรงๆ ส่วนสคริปต์เปลี่ยนเป็น venv — restart แล้วทำงานปกติทั้งคู่ เก็บไว้แบบ venv ตามเดิมเพราะแยก dependency ได้สะอาดกว่า
1.1

ติดตั้ง FreeRADIUS + PAM module

bash
apt-get update
apt-get install -y freeradius libpam-google-authenticator
mkdir -p /etc/freeradius/ga
chown freerad:freerad /etc/freeradius/ga
chmod 2770 /etc/freeradius/ga   # setgid — enrollment app ต้อง join group freerad
1.2

ผูก PAM เข้ากับ seed file ต่อ user

user=freerad คือหัวใจของดีไซน์นี้ — บังคับให้ PAM อ่าน/เขียนไฟล์ด้วยสิทธิ์ของ user จริง "freerad" เสมอ ไม่ว่า RADIUS username ที่ล็อกอินจะเป็นใคร (จะได้ไม่ต้องสร้าง Linux account จริงให้ทุกคน)

/etc/pam.d/radiusd
auth     required   pam_google_authenticator.so secret=/etc/freeradius/ga/${USER} user=freerad
account  required   pam_permit.so
1.3

ลงทะเบียน ClearPass เป็น RADIUS client

/etc/freeradius/3.0/clients.conf
client clearpass {
    ipaddr = <clearpass-ip>
    secret = <shared-secret>
    require_message_authenticator = true   # BlastRADIUS mitigation — ใส่ทั้ง 2 ฝั่ง
}
client localhost {
    secret = testing123
    require_message_authenticator = true
}
1.4

Harden systemd (non-root, ยัง sudo ได้)

unit ของ distro บางตัวใส่ ReadOnlyDirectories=/etc/freeradius/ มาด้วย ซึ่งบล็อกการเขียน seed file — ต้อง override ทับ

/etc/systemd/system/freeradius.service.d/override.conf
[Service]
ReadWriteDirectories=/etc/freeradius/ga
อย่าใส่ NoNewPrivileges=true ในหน่วย systemd ของ enrollment app ถ้า app นั้นต้องเรียก sudo ภายใน — flag นี้บล็อก sudo เงียบๆ ไม่มี error log ชัดเจน ไล่นานมาก
1.5

Deploy self-service enrollment app

Flask app เล็กๆ รันเป็น user แยก (เช่น mfaenroll) ที่ join group freerad ไว้ — endpoint หลักที่ต้องมี: /api/check (ตรวจรหัสผ่าน+สถานะ enroll), /enroll (แสดง QR), /enroll/confirm, /reset + /api/reset (self-service reset), /route-enroll-check (เช็คสถานะ enroll แบบ redirect เดียว ไม่ยิง fetch ข้าม origin)

bash
useradd -r -s /usr/sbin/nologin mfaenroll
usermod -aG freerad mfaenroll
# narrow sudoers rule — ใช้แค่ chown ไฟล์ seed หลังสร้างใหม่
echo 'mfaenroll ALL=(root) NOPASSWD: /usr/bin/chown freerad\:freerad /etc/freeradius/ga/*' \
  > /etc/sudoers.d/mfaenroll-chown
อย่าทำ TLS/HTTPS บน enrollment app นี้ — เทสมาแล้วว่าใช้ไม่ได้จริงกับ guest device ทั่วไป: ต้องลง internal CA cert เองในเครื่อง user ทุกคน ซึ่งไม่ practical สำหรับ guest wifi เลย ปล่อยเป็น plain HTTP บน port ภายใน (เช่น 8443) แล้วปิดช่องโหว่ด้วย walled-garden ACL แทน (ดู Part 4) — รายละเอียดเหตุผลอยู่ใน Gotcha #3–4
1.6

Verify ก่อนไปต่อ — ห้ามข้าม

ต้องเห็น seed file, ต้อง radtest/radclient ผ่านจากเครื่องนี้เองก่อน ค่อยไปแตะ ClearPass

bash
freeradius -X   # รันแบบ foreground ดู log สดๆ ระหว่างเทส
# terminal อีกอัน:
radtest <username> <6-digit-otp> localhost 0 testing123
02

ClearPass Policy Manager

3 Service + 1 Enforcement Policy — เป็นส่วนที่พลาดง่ายที่สุดเรื่อง ลำดับ ของ Service list ดู Gotcha #1 ก่อนทำ

🧰
ก่อนเริ่ม Part 2–4 ทั้งหมด — ลองใช้ site-kit-generator.html ก่อน (หรือ CLI: python3 generate-site-kit.py) กรอกค่าของ site นี้ครั้งเดียว (IP, secret 3 ชุด, ชื่อ object เดิมบน controller) จะได้คำสั่ง Part 1 พร้อมรัน, cheat sheet ค่าที่ต้องกรอกใน GUI ของ Part 2–3 ครบทุกช่อง, และ script dry-run/live ของ Part 4 ทันทีในเบราว์เซอร์ — ลดโอกาสพิมพ์ผิดหรือสลับ secret ระหว่างขั้นตอน (คำนวณในเบราว์เซอร์ล้วนๆ ไม่ส่งออกไปไหน แต่ผลลัพธ์มี secret จริง ไม่ commit เข้า git)
2.1

Enforcement Policy — สร้างก่อน เพราะ 3 Service ในขั้นถัดไปจะอ้างอิงชื่อนี้

Configuration → Enforcement → Policies → Add

Fieldค่า
NameMFA-OTP-Passthrough-Allow
Enforcement TypeRADIUS
Default Profile[Allow Access Profile]

ไม่ต้องเพิ่ม rule ในแท็บ Rules — ปล่อยว่างไว้แล้ว Save ได้เลย (accept/reject ตัวจริงตัดสินที่ proxy target ใน 2.3 ไม่ใช่ที่นี่ ไม่ต้องมี accounting หรือ CoA ในดีไซน์นี้)

2.2

Network Device — ลงทะเบียน enrollment VM เป็น RADIUS client ของ ClearPass

Configuration → Network → Devices → Add

Fieldค่า
Nameเช่น mfa-enroll-vm
IP or Subnet Address<enrollment-vm-ip> (VM ใน Part 1)
RADIUS Shared Secretsecret ชุด "enroll" — ต้องตรงกับ CLEARPASS_RADIUS_SECRET ใน /opt/mfa-enroll/env บน VM
Vendor NameAruba
Enable RADIUS Dynamic Authorization✔ ติ๊ก, port 3799
2.3

Proxy Target → FreeRADIUS VM (ชี้ทางกลับ — ClearPass → VM)

Configuration → Network → Proxy Targets → Add

Fieldค่า
Nameเช่น FreeRADIUS-MFA-OTP
Hostname<enrollment-vm-ip> (เครื่องเดียวกับ 2.2)
ProtocolRADIUS
Shared Secretsecret ชุด "proxy" — ต้องตรงกับ client clearpass ใน clients.conf บน VM
RADIUS Auth / Acct Port1812 / 1813 (default ไม่ต้องแก้)
2.2 กับ 2.3 ใช้ secret คนละชุดกัน — 2.2 (Network Device) คือ secret ที่ VM ใช้คุยกลับมาหา ClearPass, 2.3 (Proxy Target) คือ secret ที่ ClearPass ใช้คุยไปหา VM ถ้าสลับกันจะเช็ค OTP ไม่ผ่านเลย ไม่มี error ชัดเจนให้ไล่
2.4

สร้าง 3 Services

Configuration → Services → Add Service — ทำ 3 รอบตามตารางนี้ ทุก Service ผูก Enforcement Policy (แท็บ Enforcement) เป็น MFA-OTP-Passthrough-Allow จาก 2.1 เหมือนกันหมด

ServiceTypeRuleทำหน้าที่
MFA-OTP-Verify-
via-FreeRADIUS
RADIUS ProxyNAD-IP-Address
= <controller-ip>
ส่ง OTP ต่อไปให้ FreeRADIUS ตรวจ (Proxy Targets แท็บ → 2.3)
MFA-Step1-
Password-LocalUser
RADIUS Enforcement
(Generic)
NAD-IP-Address
= 127.0.0.1
AND Radius:Aruba
Aruba-Port-Id
= mfa-step1-login
ตรวจรหัสผ่านตอน Guest page เรียก Pre-Auth Check เอง
MFA-Enroll-
Password-Check
RADIUS Enforcement
(Generic)
NAD-IP-Address
= <enrollment-vm-ip>
ตรวจรหัสผ่านตอน enrollment app เรียกเอง

2 Service ล่างใช้ Authentication Methods = [PAP], Authentication Sources = [Local User Repository] (หรือ directory จริงของ site นั้น) — เงื่อนไข Match ALL/ANY ตั้งที่แท็บ Service (ปุ่ม Add Rule), Methods/Sources ตั้งที่แท็บ Authentication

2.5

เรียงลำดับ Service ให้ถูก — ห้ามข้าม

ในหน้า Services ลาก MFA-OTP-Verify-via-FreeRADIUS ขึ้นไปอยู่เหนือ service เดิมทุกตัวที่อาจ match แบบกว้างๆ (เช่น service 802.1X wireless เก่าที่เช็คแค่ NAS-Port-Type) — กดปุ่ม Reorder มุมขวาบน แล้วลากบรรทัดขึ้น

ClearPass หยุดที่ Service ตัวแรกที่ match ก่อน ไม่สนว่าตัวหลังตรงกว่า — ถ้าลืมขั้นนี้ ผลคือ OTP ที่ถูกต้องจะโดน reject ทั้งที่ radtest ตรงๆ ผ่านสบาย (Gotcha #1)
มี REST API script อัตโนมัติสำหรับ Part 2–3 อยู่แล้ว (clearpass-api-setup.py) แต่ยังใช้กับ pilot site นี้ไม่ได้ — root cause ยืนยันแล้วว่า ClearPass Guest's Apigility (PHP API framework) ขาดไฟล์ /opt/amigopod/www/_include/Apigility/config/application.config.php บน disk ของเครื่องนี้จริงๆ (เห็น error นี้ใน Guest → Administration → Support → Application Log ทุกครั้งที่ยิง API, ทดสอบซ้ำล่าสุด 8 ก.ย. 2026 ด้วย API Client ใหม่ก็ยังเป็นแบบเดิม — HTTP 200 body ว่างเปล่า ไม่ใช่ปัญหา client_id/secret) เป็นเรื่องที่ต้องแจ้งทีมดูแล ClearPass ตัวนี้ไปซ่อม/ติดตั้ง Apigility ใหม่ ระหว่างนี้ให้ทำตามขั้นตอน manual ด้านบนแทน
03

ClearPass Guest — 2 Web Login Pages

Step 1 เช็ครหัสผ่าน (ไม่แตะ controller จริง) → Step 2 เช็ค OTP (ยิงตรงไป controller จริง) — ทั้งคู่ skin เดียวกันได้ แต่ field setting ต่างกันชัดเจน

3.1

เปิดหน้าสร้าง Web Login

Menu → Guest (เปิด ClearPass Guest คนละหน้าต่างจาก Policy Manager) แล้ว Configuration → Pages → Web Logins → Create a new web login page — ทำ 2 รอบ ตามข้อ 3.2 และ 3.3

3.2

หน้าที่ 1 — mfa-step1-login (เช็ครหัสผ่าน)

Fieldค่า
NameMFA-Step1-Password-Login
Page Namemfa-step1-login — ต้องตรงกับ Service MFA-Step1-Password-LocalUser ใน Part 2.4 เป๊ะ
Vendor SettingsAruba
Login MethodPolicy-initiated
AuthenticationCredentials — Require a username and password
Pre-Auth CheckRADIUS → service ที่ match 127.0.0.1
Default URLURL ของหน้า Step 2 (3.3) — เช่น https://<clearpass-ip>/guest/mfa-otp-login.php
Force default destination✔ ต้องติ๊ก — ดู Gotcha #2

ใส่ลิงก์ "First time connecting?" ไปหน้า enroll และลิงก์ "Lost your authenticator? Reset it here" ไปหน้า reset ไว้ใน Header/Footer HTML เป็น <a href> ธรรมดา (ห้ามเป็น cross-origin fetch/form — ดู Gotcha #3–4)

3.3

หน้าที่ 2 — mfa-otp-login (เช็ค OTP)

Fieldค่า
NameMFA-OTP-Guest-Login
Page Namemfa-otp-login
Vendor SettingsAruba
Login MethodController-initiated
AddressIP ของ controller จริง
AuthenticationCredentials — Require a username and password
Pre-Auth CheckNone — ดู Gotcha #5
Password Label"OTP Code:" (custom label)
3.4

UX เสริม (ไม่บังคับ แต่ประสบการณ์ user ดีขึ้นมาก)

สคริปต์เล็กๆ ใน Footer HTML ของทั้ง 2 หน้า ทำ 3 อย่าง โดยใช้ sessionStorage ล้วนๆ (ไม่ใช่ fetch ข้าม origin):

แนวคิดของสคริปต์ (pseudocode)
// หน้า Step 1 — ดัก username ตอนกด submit/Enter
sessionStorage.setItem('mfa_username', usernameFieldValue)

// หน้า Step 2 — โหลดปุ๊บ:
1) fill + ซ่อนช่อง username (เหลือแค่กรอก OTP)
2) redirect (ธรรมดา ไม่ใช่ fetch) ไปเช็คว่า enroll แล้วหรือยัง
   → enroll แล้ว: bounce กลับมาหน้าเดิมพร้อม ?checked=1
   → ยังไม่ enroll: ส่งตรงไปหน้า enroll พร้อม prefill username
04

Aruba Controller (ArubaOS 8)

สมมติว่า SSID / AP-group / VLAN มีอยู่แล้ว — ส่วนนี้ทำแค่ 3 อย่าง: เปิด walled-garden ให้ guest คุยกับ VM ได้, ผูก RADIUS ไปหา ClearPass, sync secret

Automated, live-tested against the real controller (2026-09-08): ทั้ง Part 4 (4.1–4.3) ทำแทนได้ด้วย provision-controller.py — SSH เข้า controller เอง (10.69.11.63) ยิงคำสั่งตามลำดับด้วยชื่อ object จริงของ site นี้ (hpe-guest_cppm_prof, cppm-12, HPE-Guest_dot1_svg) และ secret จริง ปิดท้ายด้วย write memory — transcript ออกมาไม่มี % Invalid input เลยสักบรรทัด, Configuration Saved ชัดเจน หลังรันตรวจซ้ำด้วย show rights / show aaa server-group / show netdestination สดจาก CLI: mfa-enroll-whitelist ยังอยู่ตำแหน่ง 3 ตัวเดียวไม่ซ้ำ, server-group มี auth-server cppm-12 ตัวเดียว, netdestination มีแค่ 2 host ตามที่ตั้งไว้ — ยืนยันว่ารันซ้ำได้จริง ไม่สร้าง object ซ้ำ
4.1

Walled-garden — เปิดช่องให้ guest (ยังไม่ authenticate) คุยกับ ClearPass + enrollment VM ได้

ArubaOS CLI
netdestination "mfa-walled-garden"
   host <clearpass-ip>
   host <enrollment-vm-ip>
!
netservice svc-mfa-enroll tcp 8443
!
ip access-list session mfa-enroll-whitelist
   user alias mfa-walled-garden svc-mfa-enroll permit
!
user-role <pre-auth-role-ของ-SSID>
   access-list session mfa-enroll-whitelist position 3   # position 1-2 reserved ระบบ
4.2

ผูก RADIUS server-group ให้ captive portal ยิงไป ClearPass

ArubaOS CLI
aaa authentication-server radius cppm-otp
   host <clearpass-ip>
   key <shared-secret>          # ต้องตรงกับ Network Device ใน ClearPass เป๊ะ
!
aaa server-group mfa-otp-svg
   auth-server cppm-otp position 1
!
aaa authentication captive-portal "<ชื่อ-cp-profile-ของ-SSID>"
   server-group mfa-otp-svg
4.3

Verify จาก controller เองก่อนลองมือถือจริง

ArubaOS CLI
aaa test-server pap cppm-otp <username> <otp-code>
→ ควรได้ "Authentication Successful" ก่อนไปทดสอบผ่าน Wi-Fi จริง
🔑
SSH เข้า ArubaOS จาก client รุ่นใหม่ (paramiko/OpenSSH ใหม่ๆ) มักต่อไม่ติดเพราะ key exchange เก่าเกินไป ต้องเติม flag:
-o KexAlgorithms=+diffie-hellman-group14-sha1 -o HostKeyAlgorithms=+ssh-rsa

Live-Verified Configuration (Pilot Site)

เดินหน้าจอจริงผ่าน ClearPass Policy Manager, ClearPass Guest และ Aruba Controller GUI เมื่อ 5 ก.ย. 2026 เพื่อยืนยันว่าค่าที่ตั้งไว้ตรงกับที่ Part 2–4 อธิบาย — เก็บชื่อ object จริงไว้เป็นตัวอย่างอ้างอิงตอนไป deploy site ใหม่ (ชื่อจะไม่เหมือนกันทุก site แต่โครงสร้างเหมือนกัน)

ยืนยันสดจากจอจริง ไม่ใช่จากไฟล์ config หรือความจำ — เข้า Policy Manager () และ Aruba9004-LTE () ด้วย admin login แล้วเปิดดูทีละหน้าจอ ตรงกับที่ site-setup.sh และ provision-controller.py รันจริงไปตอนตั้งค่า
2.1

Network Device + Proxy Target (2 secret คนละทิศ — ดู Part 1)

Objectประเภทค่า
cppm-mfa-enrollNetwork Device · "MFA self-enrollment Flask API (radtest client for password verification)" — secret ฝั่ง VM → ClearPass
FreeRADIUS-MFA-OTPProxy Target · RADIUS · auth 1812 / acct 1813 — secret ฝั่ง ClearPass → VM
2.2

3 Services (ตรวจแล้วอยู่เหนือ service เก่าทุกตัว — Gotcha #1 ผ่าน)

ServiceRule จริงEnforcement
MFA-OTP-Verify-
via-FreeRADIUS
NAD-IP-Address = (controller) → Proxy Target: FreeRADIUS-MFA-OTPMFA-OTP-
Passthrough-Allow
MFA-Step1-
Password-LocalUser
NAD-IP-Address = 127.0.0.1 AND Radius:Aruba Aruba-Port-Id = mfa-step1-login → [Local User Repository] + [Local SQL DB], PAP
MFA-Enroll-
Password-Check
NAD-IP-Address = (enrollment VM) → auth sources เดียวกัน

MFA-OTP-Passthrough-Allow: Default Profile = [Allow Access Profile], rule เดียว (Date:Day-of-Week EXISTS) → allow เสมอ — accept/reject ตัวจริงตัดสินที่ proxy target (FreeRADIUS) ไม่ใช่ที่นี่

3.1

2 Web Login pages จริง (ClearPass Guest)

Fieldmfa-step1-loginmfa-otp-login
Login MethodPolicy-initiatedController-initiated
Pre-Auth CheckRADIUS requestNone
Address / Default URLDefault URL = https:///guest/mfa-otp-login.phpAddress =
Override Destination✔ Force default destination for all clients

Footer HTML ของหน้า Step 1 ยืนยันมี mfaCaptureUser() เก็บ username ลง sessionStorage จริง และมีลิงก์ manual enroll (<a href> ธรรมดา) ไปหา enrollment VM ตามที่ Gotcha #3–4 อธิบายไว้

4.1

Aruba Controller — role และ RADIUS จริง

Objectค่า
Pre-auth roleHPE-Guest-guest-logon — session ACL mfa-enroll-whitelist (1 rule) ติดอยู่ครั้งเดียว ไม่ซ้ำแม้รันสคริปต์ซ้ำ 3 รอบ
Auth Servercppm-12 · RADIUS ·
Server GroupHPE-Guest_dot1_svg (คนละกลุ่มกับ default/internal ของระบบ)

ชื่อจริงเหล่านี้ต่างจากค่า default ในสคริปต์ (mfa-walled-garden, cppm-otp, mfa-otp-svg) — ตอนรัน provision-controller.py กับ site นี้ต้องสั่งด้วย flag override เช่น --walled-garden-name hpe-guest_cppm_prof --radius-server-name cppm-12 --server-group-name HPE-Guest_dot1_svg เสมอ อย่าใช้ default เปล่าๆ กับ site ที่มี object เดิมอยู่แล้ว

Known Gotchas

9 จุดที่เจอมาแล้วจริงตอน pilot — เก็บไว้เป็น reference จะได้ไม่ไปเสียเวลาไล่ซ้ำที่ site ใหม่

GOTCHA 01

Service ที่กว้างกว่าแต่มาก่อนใน list ชนะเสมอ

อาการ
OTP ที่ถูกต้อง กลับโดน Reject จาก controller จริง ทั้งที่ยิงจาก radtest ตรงๆ ผ่านสบาย
สาเหตุ
มี Service เก่า (เช่น 802.1X wireless) rule กว้างกว่า (เช็คแค่ NAS-Port-Type) อยู่เหนือ service ของเรา ClearPass หยุดที่ตัวแรกที่ match ไม่สนความเจาะจง
แก้
Reorder ให้ MFA-OTP-Verify-via-FreeRADIUS ขึ้นไปอยู่เหนือ service ที่กว้างกว่าทุกตัว
GOTCHA 02

Login วนกลับหน้า Step 1 ทั้งที่รหัสผ่านถูก

อาการ
Access Tracker ยืนยันว่า ACCEPT ทุกครั้ง แต่มือถือ (iOS โดยเฉพาะ) เด้งกลับมาหน้า login เดิมวนไปเรื่อยๆ
สาเหตุ
มือถือมี background probe เช็คอินเทอร์เน็ต (เช่น captive.apple.com) ที่ยิงไปก่อนเรามี session — ClearPass เอา URL นั้นมาใช้เป็นปลายทาง redirect หลัง login แทนที่จะใช้ Default URL ที่เราตั้ง แล้ว controller ก็ดัก URL นั้นซ้ำ กลายเป็นวน
แก้
ติ๊ก "Force default destination for all clients" ในหน้า Step 1 — บังคับใช้ Default URL เสมอไม่สนว่า client ขอ URL อะไรมา
GOTCHA 03

fetch() ข้าม origin จากหน้า HTTPS ไปหา backend ของเราไม่ทำงานบนมือถือจริง

อาการ
ทดสอบบน browser ปกติผ่านหมด แต่มือถือ guest ค้างที่ "Checking..." ตลอด
สาเหตุ
เบราว์เซอร์ไม่เชื่อ TLS cert ของ backend เรา (self-signed หรือ internal CA) เพราะ guest device ไม่เคยลง CA นั้น — ลง CA ให้ guest ทุกคนไม่ practical
แก้
เปลี่ยนจาก fetch()/XHR เป็น top-level GET redirect ธรรมดา (<a href> หรือ window.location.href) — ไม่โดน TLS-trust check แบบเดียวกัน
GOTCHA 04

<form> POST จาก HTTPS ไป HTTP โดน browser เตือน "ฟอร์มไม่ปลอดภัย" บล็อกไปเลย

อาการ
เปลี่ยนมาใช้ plain HTML form (ไม่ใช่ fetch) แล้ว ยังใช้ไม่ได้ — เจอหน้าเตือนแทน
สาเหตุ
Chrome/Safari สมัยใหม่บล็อก mixed-content form submission (HTTPS page → HTTP action) โดยเฉพาะ ไม่ว่าจะ fetch หรือ form ธรรมดาก็โดน
แก้
เหมือน Gotcha #3 — ใช้ redirect/link ธรรมดาแทน form POST เสมอเมื่อข้าม origin แบบ https→http
GOTCHA 05

OTP ถูกใช้ซ้ำ 2 ครั้งในการ login เดียว — reject รอบสอง

อาการ
กรอก OTP ถูก แต่โดน reject ที่ controller จริง ทั้งที่ ClearPass Guest บอกว่าผ่าน
สาเหตุ
Pre-Auth Check ของหน้า Step 2 (ตั้งเป็น RADIUS) แอบไปตรวจ+ใช้ OTP นั้นไปแล้วรอบนึง ก่อนที่ browser จะยิงไปเช็คจริงที่ controller (ใช้ซ้ำไม่ได้เพราะ DISALLOW_REUSE)
แก้
ตั้ง Pre-Auth Check ของหน้า Step 2 เป็น None — ให้ controller เป็นจุดตรวจ OTP จุดเดียวเท่านั้น
GOTCHA 06

seed file ใหม่ สร้างเสร็จแต่ PAM reject ทันที

อาการ
/var/log/auth.log ฟ้อง "permissions (0640) are more permissive than 0600"
สาเหตุ
pam_google_authenticator กับ strict_owner ต้องการไฟล์ mode 0600 เป๊ะ และเจ้าของต้องเป็น user ที่ระบุใน user= ของ pam.d
แก้
สร้างไฟล์ด้วย mode 0600 เสมอ + chown เป็น freerad:freerad หลังสร้าง
GOTCHA 07

systemd hardening บล็อก sudo ภายใน enrollment app

อาการ
chown ไฟล์ seed fail ด้วย exit 1 เฉพาะตอนรันผ่าน systemd (รันมือนอก service ผ่านปกติ)
สาเหตุ
NoNewPrivileges=true ใน unit file บล็อก sudo -n เงียบๆ
แก้
เอา NoNewPrivileges ออกจาก service ที่ต้องเรียก sudo ภายใน
GOTCHA 08

radtest ไม่สะท้อน NAS-IP จริง

อาการ
ทดสอบผ่าน radtest แล้วเงียบ ไม่โดน Service rule ไหนจับเลยใน Access Tracker
สาเหตุ
radtest ตั้ง NAS-IP-Address เป็น 127.0.1.1 เสมอ ไม่ใช่ IP เครื่องจริงที่รัน
แก้
ใช้ radclient แล้วใส่ NAS-IP-Address เอง ให้ตรงกับ IP จริงที่ Service rule คาดหวัง
GOTCHA 09

BlastRADIUS — อย่าลืม require_message_authenticator

อาการ
ไม่ใช่ bug ที่เจอตรงๆ แต่เป็นช่องโหว่ระดับ protocol (CVE-2024-3596) ที่ RADIUS classic เจอทั้งวงการ
สาเหตุ
RADIUS ดั้งเดิมตรวจสอบ integrity ของ response อ่อนเกินไป
แก้
ใส่ require_message_authenticator = true ในทุก client block ของ clients.conf — และเช็คว่าฝั่ง proxy ของ ClearPass เปิดเหมือนกัน

Go-Live Checklist

เช็คตามลำดับ ห้ามข้าม verification step ไปทำขั้นถัดไป — สถานะเซฟไว้ในเบราว์เซอร์นี้อัตโนมัติ

0 / 0
Part 1 — FreeRADIUS + Enrollment VM
Part 2 — ClearPass
Part 3 — Guest Pages
Part 4 — Controller
ทดสอบจริงผ่าน Wi-Fi (มือถือจริง)