Thuta Learning
CI/CD with GitHub Actions
AdvancedDevOps & Toolsbeginner

CI ထဲက Supply-Chain Security

ဒီခန်းပြီးရင် ဘာတတ်သွားမလဲ

  • CI ထဲက Supply-Chain Security concept ကို နားလည်ရှင်းပြနိုင်ရန်
  • Diagram ကို ဖတ်ပြီး pipeline ထဲမှာ job တွေ ဘယ်လိုစီးဆင်းပြီး ဘာက gate လုပ်သလဲ ခြေရာခံနိုင်ရန်
  • Workflow file ကို ဖတ်ပြီး ဘယ် job တွေ ဘယ်အစဉ်လိုက် run မလဲ ကြိုတင်ပြောနိုင်ရန်

နားလည်ထားရမယ့် အချက်

ကိုယ့် pipeline ဟာ code ကို checkout လုပ်ပြီး တခြားလူတွေရဲ့ program တွေကို ကိုယ့် secret, ကိုယ့် registry, ပြီးတော့ ကိုယ့် repository ကို ရေးနိုင်တဲ့ token တစ်ခုနဲ့အတူ အလုပ်လုပ်ခိုင်းတဲ့ စက်တစ်လုံးပါ။ uses: လိုင်းတိုင်းကို production အခွင့်အရေးပါတဲ့ dependency တစ်ခုလို့ သဘောထားပါ — တကယ်လည်း အဲဒါပါပဲ။

pin လုပ်တာကနေ စပါ။ uses: some/action@v3 က tag တစ်ခုကို ဖြေရှင်းယူတာပါ၊ tag ဆိုတာ ရွှေ့လို့ရတဲ့ ညွှန်းချက်တစ်ခုပါ — အဲဒီ repository ပိုင်ရှင်က အသစ်တစ်ခုဆီ ရွှေ့လိုက်ရင် ကိုယ့်ရဲ့ နောက် run က ကိုယ် ဘယ်တုန်းကမှ မဖတ်ဖူးတဲ့ code ကို run လိုက်တာပါ။ ၄၀ လုံးပြည့် commit SHA ကတော့ မပြောင်းလဲနိုင်ဘူး၊ ဒါကြောင့် upgrade ဆိုတာ ညအိပ်နေတုန်း ကိုယ့်အပေါ် ကျရောက်လာတဲ့ အရာမဟုတ်တော့ဘဲ review လုပ်လို့ရတဲ့ pull request တစ်ခု ဖြစ်သွားတယ်။ pin လုပ်တာက action ကို ယုံကြည်ထိုက်အောင် လုပ်ပေးတာ မဟုတ်ဘူး — မပြောင်းလဲအောင် လုပ်ပေးတာပါ၊ ပြီးတော့ အဲဒါက review ကို အဓိပ္ပာယ်ရှိစေတယ်။

နောက်တစ်ခုက least privilege ပါ။ GITHUB_TOKEN ဟာ repository အဟောင်းတွေမှာ default အနေနဲ့ ရက်ရောစွာ ခွင့်ပြုထားတယ်။ workflow အဆင့်မှာ permissions: contents: read လို့ ကြေညာပြီး လိုအပ်တဲ့ job မှာပဲ ပိုပေးပါ။ အဲဒါဆိုရင် test job ထဲက အန္တရာယ်ရှိသွားတဲ့ step တစ်ခုက commit push လုပ်လို့လည်း မရ၊ release ထုတ်လို့လည်း မရဘူး — သူခိုးယူနိုင်တဲ့ token က အဲဒါတွေ လုပ်ခွင့် မရှိလို့ပါ။

အထက်မြက်ဆုံး အနားသတ်က pull_request_target ပါ။ သူ ရှိရတဲ့ အကြောင်းက fork ကလာတဲ့ pull request တွေ secret ကို သုံးနိုင်ဖို့ပါ — ပြီးတော့ အန္တရာယ်ရှိရတဲ့ အကြောင်းရင်းကလည်း အတိအကျ အဲဒါပါပဲ။ သူက base repository ရဲ့ context ထဲမှာ secret တွေ ပါလျက် run တယ်၊ ဒါကြောင့် pull request ရဲ့ code ကို checkout လုပ်ပြီး run လိုက်တာနဲ့ မသိတဲ့ လူစိမ်းတစ်ယောက်ဆီ ကိုယ့် credential တွေကို လက်ဆင့်ကမ်းလိုက်တာ ဖြစ်တယ်။ မယုံရတဲ့ code ကို run တဲ့ အရာအားလုံးအတွက် pull_request ကို သုံးပါ၊ pull_request_target ကို contributor ရဲ့ ပြောင်းလဲမှုကို လုံးဝ မ run တဲ့ step တွေအတွက်ပဲ ချန်ထားပါ။

နောက်ဆုံးအနေနဲ့ သက်တမ်းရှည် cloud key တွေ သိမ်းထားတာကို ရပ်ပါ။ OIDC က workflow တစ်ခုကို လက်မှတ်ထိုးထားတဲ့ identity token နဲ့ သက်တမ်းတိုတဲ့ credential လဲလှယ်ခွင့် ပေးတယ်၊ အဲဒီ role က ကိုယ့် repository နဲ့ branch ကိုပဲ ယုံကြည်တယ် — ပေါက်ကြားစရာ တာရှည်ခံ လျှို့ဝှက်ချက် ဘာမှ မကျန်တော့ဘူး။

text
ATTACK PATH THROUGH A COMPROMISED ACTION
----------------------------------------
  attacker gains push access to a popular action's repository
         |
         v
  moves the v3 tag to a new commit  (tags are mutable)
         |
         v
  your workflow: uses: some/action@v3
         |
         v
  their code now runs INSIDE your job, with:
      - every secret exposed to that job
      - GITHUB_TOKEN and whatever scopes it holds
      - network access to send all of it anywhere

WHERE THE PATH IS CUT
  pin @<40-char-sha>        the moved tag never reaches you
  permissions: contents:read a stolen token cannot write or publish
  OIDC short-lived creds     nothing long-lived is left to steal
  avoid pull_request_target  untrusted PR code never sees secrets

လက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်

ဒီ workflow မှာ ဂရုစိုက်ရမယ့် အချက် သုံးခု ရှိတယ်။ ပထမက actions/checkout နဲ့ actions/setup-node ကို tag နဲ့ မဟုတ်ဘဲ ၄၀ လုံးပြည့် commit SHA နဲ့ pin လုပ်ထားတာပါ။ နောက်က comment နဲ့ ရေးထားတဲ့ version က လူဖတ်ဖို့ပဲ ဖြစ်ပြီး တကယ် run တာက SHA ပါ။ ကိုယ်တိုင် လုပ်တဲ့အခါ SHA ကို အဲဒီ action ရဲ့ release page ကနေ ကူးယူပါ — google က ရလာတဲ့ ဟာ မဟုတ်ဘဲ ကိုယ်တိုင် အတည်ပြုထားတဲ့ ဟာ ဖြစ်ရမယ်။ Dependabot က SHA pin တွေကို upgrade PR အဖြစ် ဖွင့်ပေးနိုင်လို့ pin လုပ်တာဟာ dependency အဟောင်းတွေ ကျန်နေစေတယ် ဆိုတဲ့ အငြင်းအခုံကို ဖြေရှင်းပြီးသားပါ။

ဒုတိယက permissions ပါ။ workflow အဆင့်မှာ contents: read လို့ ကြေညာထားပြီး build job မှာမှ id-token: write ကို ထပ်ထည့်ထားတယ် — အဲဒါက OIDC token တောင်းဖို့ လိုအပ်တဲ့ တစ်ခုတည်းသော အပိုခွင့်ပါ။ scan job မှာတော့ ဘာမှ မထပ်ထည့်ထားလို့ ပိုနည်းတဲ့ အခွင့်အရေးနဲ့ run တယ်။

တတိယက credential ပါ။ aws-actions/configure-aws-credentials ကို access key နဲ့ မဟုတ်ဘဲ role-to-assume နဲ့ သုံးထားတယ်။ AWS ဘက်မှာ အဲဒီ role ရဲ့ trust policy ကို ကိုယ့် repository နဲ့ branch အတိအကျအတွက်ပဲ ကန့်သတ်ထားရမယ် — မဟုတ်ရင် ဘယ် repository ကမဆို token ရနိုင်သွားပြီး OIDC ရဲ့ အကျိုးကျေးဇူး ကုန်သွားပါလိမ့်မယ်။

အတူတူ စမ်းရေးကြည့်မယ်

yaml
name: Secure Build
on:
  pull_request:
    branches:
      - main
permissions:
  contents: read
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      # Third-party actions are pinned to a full commit SHA so that a moved
      # tag cannot silently change the code that runs with our secrets.
      # Copy the SHA from the action's own releases page when upgrading.
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8
        with:
          node-version: '20'
          cache: npm
      - run: npm ci
      - name: Get short-lived cloud credentials through OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_BUILD_ROLE }}
          aws-region: ap-southeast-1
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          tags: app:${{ github.sha }}
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - name: Fail if any action is referenced by a mutable tag
        run: ./scripts/check-action-pins.sh .github/workflows
      - name: Audit dependencies for known advisories
        run: ./scripts/audit-dependencies.sh
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: security-report
          path: reports/
          retention-days: 30
You should see
pull request တစ်ခုပေါ်မှာ build နဲ့ scan job နှစ်ခု တစ်ပြိုင်တည်း run တယ်။ build job က pin လုပ်ထားတဲ့ SHA အတိအကျမှာ ရှိတဲ့ action code ကိုပဲ run တယ် — အဲဒီ action ရဲ့ tag ကို နောက်ပိုင်း ရွှေ့လိုက်လည်း ကိုယ့် run က မပြောင်းဘူး။ OIDC step က AWS ဆီက သက်တမ်းတိုတဲ့ credential ကို job အတွင်းမှာပဲ ရယူတယ်၊ ဒါကြောင့် repository ထဲမှာ သိမ်းထားတဲ့ access key မလိုတော့ဘူး၊ ပြီးတော့ job ပြီးတာနဲ့ credential က သက်တမ်းကုန်သွားတယ်။ id-token: write ကို build job မှာပဲ ပေးထားလို့ scan job က OIDC token လုံးဝ တောင်းလို့ မရဘူး။ scan job ရဲ့ pin စစ်တဲ့ step က workflow file တစ်ခုခုမှာ mutable tag နဲ့ ရေးထားတဲ့ action တွေ့ရင် run ကို fail စေတယ်၊ report ကတော့ if: always() ကြောင့် အမြဲ တင်ပြီးသား ဖြစ်နေမယ်။

၅ မိနစ် စမ်းကြည့်

ကိုယ့် .github/workflows ထဲက uses: လိုင်းအားလုံးကို စာရင်းလုပ်ပြီး tag နဲ့ ရေးထားတာ ဘယ်နှစ်ခုလဲ ရေတွက်ပါ။ third-party (GitHub ကိုယ်တိုင် မထုတ်တဲ့) ဟာတွေကို အရင် SHA နဲ့ pin လုပ်ပါ။ ပြီးရင် workflow တိုင်းရဲ့ ထိပ်မှာ permissions: contents: read ထည့်ပြီး ဘယ် job တွေ ကျသွားလဲ ကြည့်ပါ — ကျတဲ့ job တစ်ခုချင်းစီက တကယ် ဘယ် scope လိုအပ်လဲ ဆိုတာ ဖော်ပြပေးနေတာပါ။ နောက်ဆုံး pull_request_target သုံးထားတဲ့ workflow ရှိရင် အဲဒါက PR ရဲ့ code ကို checkout လုပ်လားလို့ စစ်ပါ။

သတိလေးတစ်ချက်

pull_request_target ကို သုံးပြီး PR ရဲ့ head ref ကို checkout လုပ်မိတာ — fork ကလာတဲ့ လူစိမ်းရဲ့ code က ကိုယ့် secret တွေနဲ့အတူ run သွားတယ်

action ကို v4 လို major tag နဲ့ pin လုပ်ထားပြီး "pin လုပ်ပြီးပြီ" လို့ ထင်နေတာ — major tag ဟာ ရွှေ့လို့ရတဲ့ ညွှန်းချက်သာ ဖြစ်ပြီး commit အသစ်တွေ အမြဲ ဝင်လာနေတယ်

GitHub Docs: Security hardening for GitHub ActionsCI/CD with GitHub Actions

ဒီနေရာမှာ လူအများမှားတတ်တယ်

  • pull_request_target ကို သုံးပြီး PR ရဲ့ head ref ကို checkout လုပ်မိတာ — fork ကလာတဲ့ လူစိမ်းရဲ့ code က ကိုယ့် secret တွေနဲ့အတူ run သွားတယ်
  • action ကို v4 လို major tag နဲ့ pin လုပ်ထားပြီး "pin လုပ်ပြီးပြီ" လို့ ထင်နေတာ — major tag ဟာ ရွှေ့လို့ရတဲ့ ညွှန်းချက်သာ ဖြစ်ပြီး commit အသစ်တွေ အမြဲ ဝင်လာနေတယ်
  • Workflow ကို production branch ပေါ် တိုက်ရိုက်မစမ်းဘဲ branch (သို့) test repository တွင် အရင်အတည်ပြုပါ။

လေ့ကျင့်ခန်း

ကိုယ့် .github/workflows ထဲက uses: လိုင်းအားလုံးကို စာရင်းလုပ်ပြီး tag နဲ့ ရေးထားတာ ဘယ်နှစ်ခုလဲ ရေတွက်ပါ။ third-party (GitHub ကိုယ်တိုင် မထုတ်တဲ့) ဟာတွေကို အရင် SHA နဲ့ pin လုပ်ပါ။ ပြီးရင် workflow တိုင်းရဲ့ ထိပ်မှာ permissions: contents: read ထည့်ပြီး ဘယ် job တွေ ကျသွားလဲ ကြည့်ပါ — ကျတဲ့ job တစ်ခုချင်းစီက တကယ် ဘယ် scope လိုအပ်လဲ ဆိုတာ ဖော်ပြပေးနေတာပါ။ နောက်ဆုံး pull_request_target သုံးထားတဲ့ workflow ရှိရင် အဲဒါက PR ရဲ့ code ကို checkout လုပ်လားလို့ စစ်ပါ။

You'll know it worked when: pull request တစ်ခုပေါ်မှာ build နဲ့ scan job နှစ်ခု တစ်ပြိုင်တည်း run တယ်။ build job က pin လုပ်ထားတဲ့ SHA အတိအကျမှာ ရှိတဲ့ action code ကိုပဲ run တယ် — အဲဒီ action ရဲ့ tag ကို နောက်ပိုင်း ရွှေ့လိုက်လည်း ကိုယ့် run က မပြောင်းဘူး။ OIDC step က AWS ဆီက သက်တမ်းတိုတဲ့ credential ကို job အတွင်းမှာပဲ ရယူတယ်၊ ဒါကြောင့် repository ထဲမှာ သိမ်းထားတဲ့ access key မလိုတော့ဘူး၊ ပြီးတော့ job ပြီးတာနဲ့ credential က သက်တမ်းကုန်သွားတယ်။ id-token: write ကို build job မှာပဲ ပေးထားလို့ scan job က OIDC token လုံးဝ တောင်းလို့ မရဘူး။ scan job ရဲ့ pin စစ်တဲ့ step က workflow file တစ်ခုခုမှာ mutable tag နဲ့ ရေးထားတဲ့ action တွေ့ရင် run ကို fail စေတယ်၊ report ကတော့ if: always() ကြောင့် အမြဲ တင်ပြီးသား ဖြစ်နေမယ်။