EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Network / Cloudflare token โดน 9109 ทั้งที่ IP อยู่ใน allowlist เพราะออกทาง IPv6
Network

Cloudflare token โดน 9109 ทั้งที่ IP อยู่ใน allowlist เพราะออกทาง IPv6

โดย Ton-Or ·27 ต.ค. 2026·อ่าน 11 นาที
Cloudflare token โดน 9109 ทั้งที่ IP อยู่ใน allowlist เพราะออกทาง IPv6

allowlist ที่มีแต่ IPv4 ใช้ได้ดีจนวันที่เครื่องสักเครื่องเลือกออกทาง IPv6 แล้วทุกอย่างพังโดยที่ IP ใน list ไม่ผิดเลยสักตัว การเดาชื่อ ISP ไม่ช่วยอะไร สิ่งที่ช่วยคือเอาหลักฐานมาดูว่าใครเรียก API จากที่ไหนจริง

สรุปใน 1 นาที

อาการ/ปัญหา
Cloudflare token ที่ล็อก IP allowlist ได้ error 9109 บนเครื่องหนึ่ง ทั้งที่ list ครอบช่วงของ ISP แล้ว
ใครควรอ่าน
คนที่ใช้ Cloudflare API token แบบจำกัด IP จากหลายเครื่องหรือหลาย ISP
ทางแก้แบบสั้น
เปิด audit log ดู IP ที่เรียกจริง พบว่าเครื่องนั้นออกทาง IPv6 แล้วเติม IPv6 /32 ของ ISP เข้า allowlist โดยไม่ต้องเปลี่ยนค่า token
ข้ามไปขั้นตอน

พอดี token ของ Cloudflare ที่ผมล็อกด้วย IP allowlist ไว้ ใช้งานมาเรื่อย ๆ จนวันหนึ่งเครื่องอีกเครื่องหนึ่ง (ผมเรียกว่า host-b ในบทความนี้) ดึงสถิติไม่ได้ ได้ error 9109 กลับมา ทั้งที่เครื่องหลักยังใช้ token แบบเดียวกันได้ปกติ และ IP ที่ผมรู้อยู่ว่าเป็นของ ISP เดียวกันก็อยู่ใน list หมดแล้ว เลยต้องนั่งไล่หาว่าอะไรกันแน่ที่ไม่ตรง บทความนี้เล่าตามลำดับที่ผมเดาผิดก่อน แล้วค่อยหาหลักฐานจนเจอสาเหตุ ส่วน IP ช่วง ASN และชื่อ token ในบทความผมเปลี่ยนเป็นค่าตัวอย่างทั้งหมด

อาการ: token เดียวกัน เครื่องหนึ่งผ่าน อีกเครื่องโดน 9109

เหตุที่ผมล็อก token ด้วย Client IP Address Filtering ก็เพราะเน็ตบ้านเป็น CGNAT ที่ IP ขาออกหมุนเปลี่ยนได้บ่อย การไม่ล็อกอะไรเลยแปลว่า token รั่วไปที่ไหนก็ใช้ได้ที่นั่น ผมเลยอยากให้ใช้ได้เฉพาะจากเครือข่ายของ ISP ที่ผมใช้อยู่ ส่วนเหตุผลว่าทำไมต้องใช้ทั้งช่วง ไม่ใช่ IP เดียว เคยเล่าไว้สั้น ๆ ใน callout ของ บทความเรื่อง DNS กับเรคคอร์ดเมลสีเทา แล้ว บทความนี้เป็นภาคต่อที่ลงลึกเรื่อง "หาช่วงยังไง" กับ "กับดักที่ยังเหลือ"

ตอนแรก list มีแค่ 3 รายการ คือ /16 สองช่วงกับ /32 อีกหนึ่ง ใช้ได้ตราบเท่าที่ IP ขาออกยังตกอยู่ในสอง /16 นั้น เจ้าของงานสังเกตว่าบางวัน IP ที่เห็นไม่อยู่ในกลุ่มที่คุ้น แล้วพอมี host-b มาใช้ token ด้วยก็ได้ 9109 ทันที

3รายการใน list ตอนแรก
5 IP / 9 วันIP ขาออกจากตัวเฝ้า IP
586prefix ที่ ASN ประกาศ
417IP ใน audit log 17 เดือน

เดาผิดไปสองรอบก่อนจะมีหลักฐาน

รอบแรกผมเดาว่า ISP ในเครือเดียวกันอาจใช้คนละ ASN (มือถือกับ ISP อีกเจ้าที่ควบรวมกัน) เลยไปหาบล็อกของเครือมาเพิ่มเข้า list ทำเสร็จก็ไม่ใช่ปัญหาของ host-b เลย 9109 ยังอยู่เหมือนเดิม

รอบสองผมเดาว่า IP ที่ "ไม่อยู่ในกลุ่ม" ที่เจ้าของงานเห็นน่าจะเป็นมือถือ VPN หรือ WARP ของคนอื่น ข้อนี้เช็กได้ง่ายกว่าที่คิด เพราะผมมีตัวเฝ้า IP ขาออกของเครื่องเราอยู่แล้ว ย้อนดูได้ว่า 9 วันที่ผ่านมาเครื่องเรามี IP ขาออก 5 ค่า และทั้ง 5 ค่าอยู่ใน pool เดียวกันหมด ไม่มีค่าไหนหลุดออกไปนอก /16 ที่อนุญาตไว้

เดาจากชื่อ ISP แล้วไม่ได้อะไร

สองข้อเดาของผมมีรูปเดียวกัน คือเริ่มจากชื่อผู้ให้บริการแล้วคิดต่อเอา ซึ่งไม่มีอะไรมาพิสูจน์ว่าถูกหรือผิด ที่ไม่ได้เสียเวลามากกว่านี้เพราะผมหันไปหาข้อมูลที่ตรวจสอบได้สองชุด ชุดแรกคือ prefix ที่ ASN ประกาศจริง ชุดที่สองคือ audit log ว่าใครเรียกบัญชีนี้จาก IP ไหน

หาช่วง IP ของ ISP จาก RIPEstat แทนการเดา

RIPEstat มี endpoint announced-prefixes ที่บอกว่า ASN หนึ่งประกาศ prefix อะไรอยู่บ้าง ใส่ ASN ของ ISP ลงไปก็ได้รายการจริงที่อยู่บนอินเทอร์เน็ตตอนนี้ ในบทความผมใช้ AS64496 เป็นค่าตัวอย่าง ของจริงให้ใส่ ASN ของผู้ให้บริการที่คุณใช้เอง

import ipaddress, json, urllib.request

ASN = "AS64496"   # ค่าตัวอย่าง ใส่ ASN ของ ISP ตัวเอง
url = f"https://stat.ripe.net/data/announced-prefixes/data.json?resource={ASN}"
with urllib.request.urlopen(url, timeout=30) as r:
    prefixes = [p["prefix"] for p in json.load(r)["data"]["prefixes"]]

# collapse_addresses ต้องส่งทีละ version จะปนกันไม่ได้
v4 = [ipaddress.ip_network(p) for p in prefixes if ":" not in p]
v6 = [ipaddress.ip_network(p) for p in prefixes if ":" in p]
blocks4 = list(ipaddress.collapse_addresses(v4))
blocks6 = list(ipaddress.collapse_addresses(v6))

print(len(prefixes), "prefix ->", len(blocks4), "v4 block,", len(blocks6), "v6 block")
for b in sorted(blocks4, key=lambda n: n.prefixlen)[:10]:
    print(b)

ในเคสนี้ได้ 586 prefix พอใช้ collapse_addresses ยุบช่วงที่ซ้อนกันหรือติดกันเหลือ 53 บล็อก ในนั้นมี /16 ใหญ่ 4 ช่วงที่ list ผมยังไม่มี กับบล็อกเล็กอีก 23 รายการ ตรงนี้คืออีกกับดัก ถ้าสั่งเพิ่มทุกบล็อกเข้า list ตามที่ได้ทันที ผมก็เพิ่มช่วงของลูกค้าผู้ให้บริการรายอื่นเข้าไปด้วยโดยไม่จำเป็น ช่วงของ ISP ที่ใหญ่ ๆ มักครอบเครือข่ายคนอื่นนับพันคน การเปิด token ให้ทั้งหมดคือการลดความแคบของ allowlist ลงเยอะ จะเพิ่มอะไรเข้า list ต้องมีเหตุผลรองรับ

ผมเลยเอาข้อมูลชุดที่สองมาใช้ตัดสิน

เปิด audit log แยก IP ของเราออกจาก IP ของคนอื่น

Cloudflare เก็บ audit log ของบัญชีย้อนหลังได้ 17 เดือน ในนั้นมี IP ของผู้กระทำทุกครั้ง ผมดึงออกมานับได้ 417 IP ที่เคยเรียกบัญชีนี้ จากนั้นเอามาเทียบกับช่วงของ ISP

import collections, ipaddress, json

# audit log ที่ export มาเป็นไฟล์ JSON (แต่ละรายการมี actor.ip)
logs = json.load(open("audit_export.json", encoding="utf-8"))
ips = collections.Counter(e["actor"]["ip"] for e in logs if e.get("actor", {}).get("ip"))

# ตัวอย่าง: ในเคสจริงเป็น /16 สองช่วง ที่นี่ใช้ /24 แทนเพื่อไม่ให้เป็นค่าจริง
ours = [ipaddress.ip_network("203.0.113.0/24"), ipaddress.ip_network("198.51.100.0/24")]

mine, others = collections.Counter(), collections.Counter()
for ip, n in ips.items():
    a = ipaddress.ip_address(ip)
    (mine if any(a in net for net in ours) else others)[ip] += n

print("ในกลุ่มที่อนุญาตแล้ว:", len(mine), " นอกกลุ่ม:", len(others))
for ip, n in others.most_common(15):
    print(f"{n:5d}  {ip}")

ผลที่ได้ชัดมาก IP ของเครื่องที่ใช้ token ของเราโผล่แค่สอง pool /16 ที่อนุญาตไว้แล้วเท่านั้น ส่วน IP ของ ISP รายอื่นสองสามราย ที่ผมเคยคิดว่าต้องเพิ่ม พอไล่ดูว่าเรียกอะไรก็พบว่าเป็นตัวอัปเดต DDNS ของเว็บอื่นในบัญชีเดียวกัน ไม่ใช่เครื่องเรา ข้อสรุปเลยเป็นว่า ไม่ต้องเพิ่ม

สมุดบัญชีเล่มหนาเต็มไปด้วยรายการ IP กับแว่นขยายที่ส่องกลุ่มเล็ก ๆ ที่ถูกไฮไลต์
417 รายการใน audit log ส่วนใหญ่ไม่ใช่เครื่องเรา สิ่งที่ต้องทำคือส่องหากลุ่มเล็ก ๆ ที่เป็นของเราจริง
เดาจากชื่อ ISP

เพิ่มทุกช่วงที่ดูใกล้เคียง

list ยาวขึ้นเรื่อย ๆ แต่ไม่รู้ว่าอันไหนจำเป็น และ token เปิดให้คนอื่นใน ISP เดียวกันใช้ได้มากขึ้นโดยไม่มีเหตุผล

ดูจาก audit log

เพิ่มเฉพาะที่มีหลักฐานว่าเป็นเรา

แยกได้ว่า IP ไหนเป็นเครื่องเรา IP ไหนเป็นของคนอื่นในบัญชีเดียวกัน list สั้นกว่าและมีที่มาอธิบายได้ทุกบรรทัด

แต่ audit log บอกได้แค่ว่า IP ไหนเป็นเครื่องเรา มันไม่ได้บอกว่าทำไม host-b โดนปฏิเสธ ผมเลยกลับมาถามใหม่ว่า request ของ host-b ออกไปด้วย address แบบไหนกันแน่ เพราะที่ผมเทียบมาทั้งหมดเป็น IPv4 ล้วน

สาเหตุจริง: host-b ออก Cloudflare ทาง IPv6

host-b ต่อ Cloudflare ผ่าน IPv6 ซึ่งเป็น prefix /44 ของ ISP เดียวกัน ขณะที่ allowlist ของ token มีแต่ IPv4 ล้วน ๆ เครื่องที่มีทั้งสองทางจะเลือกเองว่าจะใช้ทางไหนต่อโฮสต์ปลายทาง ผลคือ request ที่ออกไปในรูป IPv6 ไม่ตรงกับรายการไหนเลย และโดนปฏิเสธด้วย 9109

ทำไมโดน 9109 ทั้งที่ IP อยู่ใน allowlistเครื่อง host-bออกได้ทั้ง v4 และ v6IPv4 203.0.113.xIPv6 2001:db8::x(1) เลือกเส้นทางapi.cloudflare.comเทียบ IP ต้นทาง(2) เทียบallowlist ของ token49 รายการทั้งหมดเป็น IPv4(3) ผลลัพธ์IPv4 อยู่ในรายการ ผ่านIPv6 ไม่มีในรายการ ได้ 9109
สองเส้นทางจากเครื่องเดียวกัน เส้น IPv4 ตรงกับ list ส่วนเส้น IPv6 หลุดจาก list ทั้งก้อนเพราะไม่เคยมีรายการ IPv6 อยู่เลย

วิธีตรวจว่าเครื่องหนึ่งออกทางไหน ใช้ trace ของ Cloudflare เองได้ บังคับ -4 กับ -6 เทียบกันแล้วดูบรรทัด ip=

curl -4 -s https://www.cloudflare.com/cdn-cgi/trace | grep '^ip='
# ip=203.0.113.25

curl -6 -s https://www.cloudflare.com/cdn-cgi/trace | grep '^ip='
# ip=2001:db8:b930::25

# ปล่อยให้เครื่องเลือกเอง เหมือนที่โปรแกรมทั่วไปทำ
curl -s https://www.cloudflare.com/cdn-cgi/trace | grep '^ip='

ถ้าคำสั่งที่สามได้ค่าที่ขึ้นต้นด้วย 2001: หรือรูปแบบ IPv6 อื่น แปลว่าโปรแกรมของคุณก็ออกทาง IPv6 ด้วยเหมือนกัน และ allowlist ที่มีแต่ IPv4 จะรอวันพังอยู่แล้ว ในเคสของผม ตัวเลือกที่ผมนึกได้อีกทางคือบังคับให้ client ใช้ IPv4 อย่างเดียว แต่นั่นเป็นแค่ข้อเสนอ ผมไม่ได้ทำจริงในเคสนี้ เพราะการเติมช่วง IPv6 เข้า list ตรงไปตรงมากว่า และ host-b ไม่ต้องแก้อะไร

แม่กุญแจทองเหลืองที่ประตูตู้แร็ก ติดป้าย IPv4 only ขณะที่ประตูบานกว้างอีกบานข้าง ๆ เปิดอยู่
allowlist ที่ล็อกไว้เฉพาะ IPv4 เปรียบเหมือนล็อกประตูบานเดียว ส่วนประตู IPv6 บานกว้างข้าง ๆ ไม่เคยมีใครล็อกหรืออนุญาต

เติม IPv6 /32 แล้วค่า token ไม่ต้องเปลี่ยน

ที่ผมทำคือเติม IPv6 /32 ของกลุ่ม ISP อีก 4 ช่วงเข้า allowlist เดิม list กลายเป็น 53 รายการ ค่า token ตัวเดิมไม่เปลี่ยน ไม่ต้อง deploy ใหม่ ไม่ต้องแตะ host-b สักบรรทัด ส่วนที่แก้อยู่ในฟิลด์ condition ของ token เท่านั้น

{
  "condition": {
    "request_ip": {
      "in": [
        "203.0.113.0/24",
        "198.51.100.0/24",
        "2001:db8::/32"
      ],
      "not_in": []
    }
  }
}

ตัวอย่างข้างบนย่อให้เหลือสามบรรทัด ของจริงมี IPv4 ครบ 49 รายการแล้วต่อด้วย IPv6 /32 อีก 4 ช่วง

ซ่อม 4 ขั้น: หาช่วง พิสูจน์ แล้วเติม IPv61 RIPEstat586 prefixที่ ASN ประกาศ2 collapseเหลือ 53 บล็อกipaddress3 audit log17 เดือน 417 IPของเรา 2 pool /16ที่เหลือคนอื่น4 เติม IPv6/32 อีก 4 ช่วงรวม 53 รายการค่า token เหมือนเดิม ไม่ต้องแตะ host-bไม่มีสิทธิ์ Write แก้ใน dashboard
สี่ขั้นจากหาช่วงไปจนเติม IPv6 กล่องขวาล่างคือทางที่ต้องไปเมื่อ token ที่ใช้อยู่แก้ list ของตัวเองไม่ได้

ข้อที่ต้องบอกตรง ๆ คือ token อีกตัวของเรา ที่ใช้กับเครื่องอื่น มี 49 รายการ ล้วน IPv4 และผมยังไม่ได้แก้ เพราะเครื่องนั้นยังออก IPv4 อยู่ ถ้าวันไหนมันสลับไปออก IPv6 มันจะพังแบบเดียวกัน ตรงนี้เป็นบทเรียนที่ผมจดไว้ ไม่ใช่เหตุการณ์ที่เกิดขึ้นแล้ว

กับดักบน dashboard ที่เจอตอนแก้ policy

ตอนไปแก้ token ผมเจอสามเรื่องที่ไม่ได้อยู่ในคู่มือหน้าแรก ๆ

  1. แก้ token ผ่าน API ได้ ต้องใช้ token ที่มีสิทธิ์ Account API Tokens Write ถ้า token ที่ถืออยู่มีแค่ Read มันแก้ list ของตัวเองไม่ได้ ต้องไปแก้ในหน้า dashboard
  2. ตอนขยายสิทธิ์ให้ครบ policy หนึ่งใส่ permission ได้ไม่เกิน 300 ตัว ถ้าเกินจะได้ HTTP 400 ของผมต้องการเกือบ 400 กลุ่ม เลยต้องแบ่งเป็นหลาย policy
  3. policy "All zones" ที่มีสิทธิ์ระดับ account ปนอยู่ 5 ตัว (settings read, billing read และพวกเดียวกัน) แก้หรือแยกไม่ได้ จนกว่าจะย้ายสิทธิ์ระดับ account ไปไว้ใน policy ของมันเอง

ผมจัดสิทธิ์ใหม่จาก 5 policy เหลือ 2 policy จำนวนสิทธิ์รวมเท่าเดิมที่ 288 ตัว แล้วเทียบรายการทีละตัวว่าตรงกับก่อนจัด ข้อนี้สำคัญเพราะถ้าจัดแล้วสิทธิ์หายไปสักตัว มันจะไปโผล่เป็น 403 กับงานที่เคยรันได้ในวันถัดไป

token อยู่ในมือใคร จะรู้จากอะไร

อย่าวาง token เต็มชุดลงในแชต ในโน้ต หรือในบทความ ทุกค่าในบทความนี้เป็นตัวอย่าง และ allowlist เป็นเพียงชั้นเสริมไม่ใช่ตัวแทนของการเก็บ token ให้ดี ถ้าสงสัยว่ารั่ว ให้ roll token ก่อน แล้วค่อยมาไล่ดูทีหลัง

สรุปสิ่งที่ผมจะเช็กก่อนทุกครั้ง

ถ้า token ที่ล็อก IP แล้วโดน 9109 ผมจะไล่ตามนี้ ข้อแรกดูว่าเครื่องที่โดนออกทาง IPv4 หรือ IPv6 ด้วย curl -4 กับ -6 เพราะเป็นข้อที่ผมเดาไม่ถึงมานานที่สุด ข้อสองดูว่า allowlist ครอบทั้งสองตระกูลของ address หรือเปล่า ข้อสามก่อนจะเพิ่มช่วงของ ISP ไหนเข้าไป เอา audit log มาแยกก่อนว่าช่วงนั้นเป็นเครื่องเราจริงหรือเป็นของคนอื่นในบัญชีเดียวกัน ข้อสี่ถ้าต้องแก้ policy ให้ดูเพดาน 300 permission กับสิทธิ์ระดับ account ที่ปนอยู่ก่อนเริ่ม

ลำดับเช็ก 9109curl -4 / -6ออกทางไหน->list มี v4 และ v6ครบสองตระกูล->audit logก่อนเพิ่มช่วง->แก้ใน dashboardถ้าไม่มีสิทธิ์ Write

ส่วนจะบังคับ client ให้ใช้ IPv4 อย่างเดียวหรือเติม IPv6 เข้า list ก็ตามสะดวกของแต่ละที่ครับ ผมเลือกเติมเพราะไม่ต้องไปแตะเครื่องที่ใช้งานอยู่ ตรงไหนที่ผมเข้าใจกลไกของ 9109 ผิดหรือคุณเจอเงื่อนไขอื่นที่ทำให้ได้ error เดียวกัน เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย