iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Cloud Native

駕馭商用容器叢集,汪洋漂流術系列 第 12

【Day 12】 實戰篇 - 憑證快要過期了 (下) / 挖出 TLS / CA

  • 分享至 

  • xImage
  •  

不重要的前言

  • 走到這邊,我們知道了 CA/TLS 怎麼被塞給 叢集/容器/程式 來做使用。
  • 窺探出整座叢集中的運行基礎單位 Pod 如何被掛上憑證後,最後一步還需要確認期限。

前言

在這一回中,我們將演示如何挖出「各個命名空間」中的「Secret」和「ConfigMap」,進而從中觀察憑證類型,提取內容資訊後,找出期限喔。

調查手法

簡單流程 / 想法說明

  1. 窮舉所有 namespaces / 或是鎖定特定 namespace 了
  2. 用指令 oc get secret -n <namespace名稱> -o 輸出格式
    # 這指令可以看出有哪些 secrets
    # 沒指名 namespace 的話,就看你的 `~/.kube/config` 裡面怎麼寫,那會決定預設的 namespace
    oc get secrets
    
    # 這兩條指令都是去挖出 default namespace 裡面的 secret 的詳細內容
    oc get secret -n default -o yaml
    oc get secret -n default -o json
    

    上述指令試圖去列出 預設的命名空間中 (default namespace) 有哪些 Secrets
    兩種格式特色是,當資料量多的時候⋯⋯
    用 yaml 相對容易給人閱讀,因為一行一行的切齊縮排。
    而 json 格式則方便快速餵給其他指令處理。

    • 輸出內容
    apiVersion: v1
    items:
    - apiVersion: v1
      data:
        .dockercfg: e30=
      kind: Secret
      metadata:
        annotations:
          kubernetes.io/service-account.name: builder
          kubernetes.io/service-account.uid: xxxxxxxxxx
          openshift.io/token-secret.name: builder-token-xxxx
          openshift.io/token-secret.value: <在這裡是一把私鑰>
        creationTimestamp: "2023-07-20T17:06:22Z"
        name: builder-dockercfg-49x2l
        namespace: default
        ownerReferences:
        - apiVersion: v1
          blockOwnerDeletion: false
          controller: true
          kind: Secret
          name: builder-token-bk66p
          uid: xxxxxxxx
        resourceVersion: "37994"
        uid: xxxxxxxx
      type: kubernetes.io/dockercfg
    - apiVersion: v1
      data:
        ca.crt: <這邊是一串 CA_CRT>
    
    • 上面這串東西他雖然是 Secret,但仔細看,他不是我們要找的東西
      https://ithelp.ithome.com.tw/upload/images/20250829/20130149dfm014mWE5.png

    在 Type 的地方...
    目標要是 kubernetes.io/tls

  3. 知道大概位置後,再來修改指令,再推送指令一次
    • 那因為 default 裡面的 secret 沒有 TLS 也沒有 CA這次來挖挖看別的域名下的東西
      oc get secrets analytics-ai-endpoint -n apiconnect
      oc get secrets analytics-ai-endpoint -n apiconnect
      
    • 看輸出的內容
      https://ithelp.ithome.com.tw/upload/images/20250829/20130149mKiSk2Y5Z3.png
    1. 確定這東西是 tls 或是 key 之後,就把吐出的資料丟給 base64 指令處理、使用 openssl 解開後,便可以拿回有效期限。
      oc get secrets analytics-ai-endpoint -n apiconnect -o jsonpath="{.data['tls\.crt']}" | base64 -d | openssl x509 -noout -subject -issuer -enddate
      
    • 輸出結果如下:
      subject=CN=analytics.xxxxxxxxxx.tw
      issuer=CN=ingress-ca
      notAfter=Apr 27 07:46:50 2027 GMT
      
  4. 順著這個脈絡,把他們通通都揪出來。

Source

#!/usr/bin/env bash
set -euo pipefail

# Inventory all namespaces for CA/cert expiration across Secrets & ConfigMaps
# Output: ca_expiry_inventory.tsv
# Requirements: oc, jq, openssl, awk, sed

OUT="ca_expiry_inventory.tsv"
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT

echo -e "Namespace\tKind\tName\tKey\tCertIndex\tSubject\tIssuer\tNotAfter(UTC)\tDaysLeft\tSerial\tSHA256" > "$OUT"

now_epoch="$(date +%s)"

# Parse all PEM certs in a file; print one TSV line per cert
# args: ns kind name key file
parse_pem_file() {
  local ns="$1" kind="$2" name="$3" key="$4" file="$5"

  # Extract multiple certs from bundle and feed each to openssl
  # Use awk to split into per-cert temporary files
  local idx=0
  awk '
    /-----BEGIN CERTIFICATE-----/ {inblk=1; fn=sprintf("%s.%d", fbase, ++idx); print > fn; next}
    inblk { print >> fn; if (/-----END CERTIFICATE-----/) {inblk=0} }
  ' fbase="$TMPDIR/cert" "$file"

  # If no certs extracted (idx==0), maybe the whole file is a single PEM
  if ! ls "$TMPDIR"/cert.* >/dev/null 2>&1; then
    cp "$file" "$TMPDIR/cert.1" 2>/dev/null || true
  fi
  
  for pem in "$TMPDIR"/cert.*; do
    [ -s "$pem" ] || continue
    idx=$((idx+1)) # display index (1-based)
    # Pull fields via openssl
    local sub iss end ser fp dt
    sub="$(openssl x509 -in "$pem" -noout -subject 2>/dev/null | sed 's/^subject= *//; s/\t/ /g')"
    iss="$(openssl x509 -in "$pem" -noout -issuer 2>/dev/null  | sed 's/^issuer= *//; s/\t/ /g')"
    end="$(openssl x509 -in "$pem" -noout -enddate 2>/dev/null | sed 's/^notAfter=//')"
    ser="$(openssl x509 -in "$pem" -noout -serial 2>/dev/null  | sed 's/^serial=//')"
    fp="$(openssl x509 -in "$pem" -noout -fingerprint -sha256 2>/dev/null | sed 's/^SHA256 Fingerprint=//')"

    # normalize enddate to epoch; some locales require explicit "UTC"/"GMT"
    local end_epoch=""
    if [ -n "$end" ]; then
      # try parse as-is first; if fail, append "UTC"
      end_epoch="$(date -u -d "$end" +%s 2>/dev/null || date -u -d "$end UTC" +%s 2>/dev/null || echo "")"
    fi
    local days=""
    if [ -n "$end_epoch" ]; then
      days=$(( (end_epoch - now_epoch) / 86400 ))
    fi

    # Print line
    printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" \
      "$ns" "$kind" "$name" "$key" "${pem##*.}" \
      "${sub:-<n/a>}" "${iss:-<n/a>}" "${end:-<n/a>}" "${days:-<n/a>}" "${ser:-<n/a>}" "${fp:-<n/a>}" \
      >> "$OUT"
  done

  # cleanup per-call extracted files
  rm -f "$TMPDIR"/cert.* 2>/dev/null || true
}

# Handle Secrets: look for data keys that look like certs (base64 encoded)
scan_secrets_in_ns() {
  local ns="$1"
  local json
  json="$(oc get secret -n "$ns" -o json 2>/dev/null || echo '{"items":[]}')"

  echo "$json" \
  | jq -r '
      .items[]?
      | {name: .metadata.name, type: (.type // ""), data: (.data // {})}
      | select((.data | length) > 0)
      | @base64
    ' \
  | while read -r row; do
      obj="$(echo "$row" | base64 -d)"
      name="$(echo "$obj" | jq -r '.name')"
      type="$(echo "$obj" | jq -r '.type')"

      # iterate keys that are likely certs
      echo "$obj" \
      | jq -r '
          .data
          | to_entries[]
          | select(.key | test("(^ca\\.crt$)|(^tls\\.crt$)|(.+\\.crt$)|(.+bundle\\.crt$)"))
          | @base64
        ' \
      | while read -r kv; do
          ent="$(echo "$kv" | base64 -d)"
          key="$(echo "$ent" | jq -r '.key')"
          b64="$(echo "$ent" | jq -r '.value')"

          # decode to file
          f="$TMPDIR/${ns}__secret__${name}__${key}.pem"
          echo "$b64" | base64 -d > "$f" 2>/dev/null || true

          # quick heuristic: only proceed if file contains a PEM header
          if grep -q "BEGIN CERTIFICATE" "$f" 2>/dev/null; then
            parse_pem_file "$ns" "Secret" "$name" "$key" "$f"
          fi
        done
    done
}

# Handle ConfigMaps: values are plaintext; pick keys that contain PEM blocks
scan_configmaps_in_ns() {
  local ns="$1"
  local json
  json="$(oc get configmap -n "$ns" -o json 2>/dev/null || echo '{"items":[]}')"

  echo "$json" \
  | jq -r '
      .items[]?
      | {name: .metadata.name, data: (.data // {})}
      | select((.data | length) > 0)
      | @base64
    ' \
  | while read -r row; do
      obj="$(echo "$row" | base64 -d)"
      name="$(echo "$obj" | jq -r '.name')"

      echo "$obj" \
      | jq -r '
          .data
          | to_entries[]
          | select(.value | contains("BEGIN CERTIFICATE"))
          | @base64
        ' \
      | while read -r kv; do
          ent="$(echo "$kv" | base64 -d)"
          key="$(echo "$ent" | jq -r '.key')"
          val="$(echo "$ent" | jq -r '.value')"

          f="$TMPDIR/${ns}__configmap__${name}__${key}.pem"
          printf "%s" "$val" > "$f"

          parse_pem_file "$ns" "ConfigMap" "$name" "$key" "$f"
        done
    done
}

# Build namespace list
NAMESPACES="$(oc get ns -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}')"

echo "$NAMESPACES" | while read -r ns; do
  [ -z "$ns" ] && continue
  # Secrets
  scan_secrets_in_ns "$ns"
  # ConfigMaps
  scan_configmaps_in_ns "$ns"
done

echo "Done. Output -> $OUT"

結論

  • 已經盤點出憑證,也找出所有憑證的有效期限了。
  • 藏在 ConfigMap 的部分也揪出來了。 這時候可以建議對應的管理者順便改成棒棒的 best practice 了!

目前拿到兩個 TSV 表格。 剩下的部分用 Excel 之類的東西去翻找查詢就好,別字幹一堆程式出來,那樣蠻浪費時間的。
https://ithelp.ithome.com.tw/upload/images/20250829/20130149JYxFg6mtbK.png

    • 剩下的工作:從昨天產出的那份表格,可以知道 Route 用了什麼網址訪問、順藤摸瓜摸出使用中的 Secret 後,來今天的表格查看 Secret 的期限。

我認為更聰明的方法,是要把這份表格的資料,塞到資料庫、或是放到 S3 搭配 Athena 用 SQL Query 的會更快。 這樣我只要專注第一份表格就行。

參考資料

  1. 鳥哥私房菜 / 第十二章、學習 Shell Scripts
    連結: https://linux.vbird.org/linux_basic/centos7/0340bashshell-scripts.php
  2. GitHub / kodekloudhub/community-faq
    連結: https://github.com/kodekloudhub/community-faq/blob/main/docs/jsonpath.md

上一篇
【Day 11】 實戰篇 - 憑證快要過期了 (中) / 記錄 Pod Mount
下一篇
【Day 13】 Deployment 和 雖然被棄坑但也需要了解的 DeploymentConfig
系列文
駕馭商用容器叢集,汪洋漂流術14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言