CDN 如何代理 origin

說明 CDN 代理 origin 的前提條件、request 歸屬,以及 Signed URL 與 Signed Cookie 的運作與差異。

發佈 ~3 分鐘 #CloudFront

問題

  • CDN 憑什麼作為 origin 的代理
  • 難道我只要有任何一個聲稱是 CDN 的服務存在,就可以把流量都導向 origin 嗎(沒有任何防護機制嗎)?
  • 這時候 request 會算是 CDN 送出去的,還是 end user 送過去的?
  • Signed URL/Cookies
    • 那 signed url/cookies 的運作流程是啥
    • 跟 s3 的 pre-signed url 差在哪?關係是啥?

CDN 如何代理 origin

CDN 能代理 origin 是基於以下的前提條件:

  1. DNS 設定要將域名指向 CDN 的 CNAME,讓所有流量都導向 CDN
  2. Origin ACL 設定為只接受指定 CDN IP 來的請求,讓其他直接對源站發送請求皆被拒絕(也就是為啥客戶跟我們要 Cloudfront IP list)
  3. Signed URL/cookies,只有被簽過的 request 才會被允許通過 CDN 到 origin
  4. Origin 可以檢查 host header 有沒有符合預期

Request 算誰的

算是 CDN 發出來的,但 CDN 會在 request 中附加 client 的真實資訊,像是下列幾項

  • X-Forwarded-For(最常見):提供使用者的原始 IP 位址
  • True-Client-IP(某些 CDN 專有):提供最初的用戶 IP
  • CF-Connecting-IP(Cloudflare):用於標示真實來源 IP

Signed URL 運作流程

  1. 長出一組 key pair

  2. 上傳 public key 到 CDN provider

  3. 用 private key 簽出一個 URL

    • 原始字串 string_to_sign 內容會包含 resource_url & expiration_time 的資訊
    • 再用 private key 加密 string_to_sign,並包在 query string 裡、送出 request
    Python Example
    import base64
    import hashlib
    import hmac
    from datetime import datetime, timedelta
    
    def generate_signed_url(resource_url, private_key, expiration_seconds):
        expiration_time = int((datetime.utcnow() + timedelta(seconds=expiration_seconds)).timestamp())
        string_to_sign = f"{resource_url}:{expiration_time}"
        
        signature = hmac.new(private_key.encode(), string_to_sign.encode(), hashlib.sha256).digest()
        encoded_signature = base64.urlsafe_b64encode(signature).decode()
    
        signed_url = f"{resource_url}?expires={expiration_time}&signature={encoded_signature}"
        return signed_url
    
    private_key = "your-secret-key"
    signed_url = generate_signed_url("https://cdn.example.com/video.mp4", private_key, 3600)
    print(signed_url)
  4. CDN 驗證簽名並提供內容

    • CDN 提取 query string 中的 signature
    • 使用 public key 驗證簽名(加密過的字串),確保 URL 未被篡改
    • 如果有被篡改的話,就沒辦法用 public 將簽名解回原本的 resource_url

萬一連結流出去,那是不是實際內容就外洩了,這樣還算安全嗎?

  • 可以限制存取時間、存取 IP、存取次數、Referer 限制

如果要做身份驗證的話,是打開連結之後再進行密碼或密鑰的驗證會比較安全?

  • Signed URL 目的是提供快速、臨時授權存取,不像登入驗證需要持續互動

為啥 Signed URL 適合靜態內容快速授權,而身份驗證適合需要持續互動的場景?

  • 身份驗證會需要 server 參與驗證,signed URL 讓內容可以直接由 CDN 提供,這樣才能真正降低 origin 的負擔
  • 所以在需要身份驗證與頻繁存取內容的情境下,會是由 server 先進行身份驗證後,再由 server 產出 signed URL,讓 CDN 來提供靜態內容,避免 server 頻繁被存取

既然 signed URL 是為了避免 server 被頻繁存取,那由 server 來算出 signed URL,就不會算是被頻繁存取嗎?

  • 產生 signed URL 是在進行加密簽章運算,比起處理大檔案的成本低很多
    • Signed URL:需要 HMAC/RSA 簽名計算
    • 提供靜態內容:需要處理大量網路請求、流量,這才是 origin server 的瓶頸
  • CDN 可以提供快取機制,一個 signed URL 可能會一天有效,不會到每秒都需要生成
  • 也可以選擇將 signed url 做在 Lambda@Edge 上,身份驗證成功後自動簽署 signed url,降低 origin server 負擔

Signed Cookies vs Signed URL

  • 主要差異:

    1. Signed URL 的加密簽章放在 query string 中,而 Signed Cookies 放在 cookie 裡
    2. Signed Cookie 的簽章內容和 Signed URL 一樣,但多了 CloudFront policy json,來限制時間、IP、資源範圍等
  • 比較表(情境差異)

    Signed URLSigned Cookie
    存取控制針對單一資源針對多個資源
    瀏覽器處理方式需手動點擊每次存取瀏覽器自動攜帶
    易用性易於分享,可能洩漏更安全,避免 URL 洩漏
    過期控制每個 URL 單獨設定整體 session 設定

為什麼 Signed Cookie 能一次允許一個範圍,而不是像 Signed URL 一次只允許一個檔案

  • Cookie 會與特定域名關聯,只要 client 訪問該域名的任何資源,這個 cookie 就會被帶在瀏覽器中
  • CloudFront policy json 中可以指定一個存取的範圍

為什麼 Cookie 會與特定域名關聯?

  • RFC-6265 的標準規範, Set-Cookie 這個 header 裡面會需要定義域名為何
Set-Cookie: session_token=abc123; Domain=example.com; Path=/; HttpOnly; Secure; Max-Age=3600;