diff --git a/.github/workflows/depsreview.yaml b/.github/workflows/depsreview.yaml
deleted file mode 100644
index b9d6d20fff4e7..0000000000000
--- a/.github/workflows/depsreview.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: 'Dependency Review'
-on: [pull_request]
-
-permissions:
- contents: read
-
-jobs:
- dependency-review:
- runs-on: ubuntu-latest
- steps:
- - name: 'Checkout Repository'
- uses: actions/checkout@v4
- - name: 'Dependency Review'
- uses: actions/dependency-review-action@v4
diff --git a/.github/workflows/go-cross.yml b/.github/workflows/go-cross.yml
deleted file mode 100644
index 324af3e956eba..0000000000000
--- a/.github/workflows/go-cross.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: Crosscompile
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- build:
- name: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- go-version: [1.24.x]
- os: [ubuntu-latest]
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Build on ${{ matrix.os }}
- if: matrix.os == 'ubuntu-latest'
- env:
- CGO_ENABLED: 0
- GO111MODULE: on
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make crosscompile
diff --git a/.github/workflows/go-healing.yml b/.github/workflows/go-healing.yml
deleted file mode 100644
index 0ea2505128ca4..0000000000000
--- a/.github/workflows/go-healing.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: Healing Functional Tests
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- build:
- name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- go-version: [1.24.x]
- os: [ubuntu-latest]
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Build on ${{ matrix.os }}
- if: matrix.os == 'ubuntu-latest'
- env:
- CGO_ENABLED: 0
- GO111MODULE: on
- MINIO_KMS_SECRET_KEY: "my-minio-key:oyArl7zlPECEduNbB1KXgdzDn2Bdpvvw0l8VO51HQnY="
- MINIO_KMS_AUTO_ENCRYPTION: on
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make verify-healing
- make verify-healing-inconsistent-versions
- make verify-healing-with-root-disks
- make verify-healing-with-rewrite
diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml
deleted file mode 100644
index 6b43086a7a14f..0000000000000
--- a/.github/workflows/go-lint.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-name: Linters and Tests
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- build:
- name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- go-version: [1.24.x]
- os: [ubuntu-latest]
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Build on ${{ matrix.os }}
- if: matrix.os == 'ubuntu-latest'
- env:
- CGO_ENABLED: 0
- GO111MODULE: on
- run: |
- sudo apt install jq -y
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make
- make test
- make test-race
diff --git a/.github/workflows/go-resiliency.yml b/.github/workflows/go-resiliency.yml
deleted file mode 100644
index 4eb2bbb90ba5c..0000000000000
--- a/.github/workflows/go-resiliency.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: Resiliency Functional Tests
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- build:
- name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- go-version: [1.24.x]
- os: [ubuntu-latest]
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Build on ${{ matrix.os }}
- if: matrix.os == 'ubuntu-latest'
- env:
- CGO_ENABLED: 0
- GO111MODULE: on
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-resiliency
diff --git a/.github/workflows/helm-lint.yml b/.github/workflows/helm-lint.yml
deleted file mode 100644
index f444106f1cdf0..0000000000000
--- a/.github/workflows/helm-lint.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: Helm Chart linting
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- release:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Install Helm
- uses: azure/setup-helm@v4
-
- - name: Run helm lint
- run: |
- cd helm/minio
- helm lint .
diff --git a/.github/workflows/iam-integrations.yaml b/.github/workflows/iam-integrations.yaml
deleted file mode 100644
index cc57a77aa70d8..0000000000000
--- a/.github/workflows/iam-integrations.yaml
+++ /dev/null
@@ -1,161 +0,0 @@
-name: IAM integration
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- iam-matrix-test:
- name: "[Go=${{ matrix.go-version }}|ldap=${{ matrix.ldap }}|etcd=${{ matrix.etcd }}|openid=${{ matrix.openid }}]"
- runs-on: ubuntu-latest
-
- services:
- openldap:
- image: quay.io/minio/openldap
- ports:
- - "389:389"
- - "636:636"
- env:
- LDAP_ORGANIZATION: "MinIO Inc"
- LDAP_DOMAIN: "min.io"
- LDAP_ADMIN_PASSWORD: "admin"
- etcd:
- image: "quay.io/coreos/etcd:v3.5.1"
- env:
- ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
- ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379"
- ports:
- - "2379:2379"
- options: >-
- --health-cmd "etcdctl endpoint health"
- --health-interval 10s
- --health-timeout 5s
- --health-retries 5
- openid:
- image: quay.io/minio/dex
- ports:
- - "5556:5556"
- env:
- DEX_LDAP_SERVER: "openldap:389"
- openid2:
- image: quay.io/minio/dex
- ports:
- - "5557:5557"
- env:
- DEX_LDAP_SERVER: "openldap:389"
- DEX_ISSUER: "http://127.0.0.1:5557/dex"
- DEX_WEB_HTTP: "0.0.0.0:5557"
-
- strategy:
- # When ldap, etcd or openid vars are empty below, those external servers
- # are turned off - i.e. if ldap="", then ldap server is not enabled for
- # the tests.
- matrix:
- go-version: [1.24.x]
- ldap: ["", "localhost:389"]
- etcd: ["", "http://localhost:2379"]
- openid: ["", "http://127.0.0.1:5556/dex"]
- exclude:
- # exclude combos where all are empty.
- - ldap: ""
- etcd: ""
- openid: ""
- # exclude combos where both ldap and openid IDPs are specified.
- - ldap: "localhost:389"
- openid: "http://127.0.0.1:5556/dex"
-
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Test LDAP/OpenID/Etcd combo
- env:
- _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }}
- _MINIO_ETCD_TEST_SERVER: ${{ matrix.etcd }}
- _MINIO_OPENID_TEST_SERVER: ${{ matrix.openid }}
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-iam
- - name: Test with multiple OpenID providers
- if: matrix.openid == 'http://127.0.0.1:5556/dex'
- env:
- _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }}
- _MINIO_ETCD_TEST_SERVER: ${{ matrix.etcd }}
- _MINIO_OPENID_TEST_SERVER: ${{ matrix.openid }}
- _MINIO_OPENID_TEST_SERVER_2: "http://127.0.0.1:5557/dex"
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-iam
- - name: Test with Access Management Plugin enabled
- env:
- _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }}
- _MINIO_ETCD_TEST_SERVER: ${{ matrix.etcd }}
- _MINIO_OPENID_TEST_SERVER: ${{ matrix.openid }}
- _MINIO_POLICY_PLUGIN_TEST_ENDPOINT: "http://127.0.0.1:8080"
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- go run docs/iam/access-manager-plugin.go &
- make test-iam
- - name: Test MinIO Old Version data to IAM import current version
- if: matrix.ldap == 'ldaphost:389'
- env:
- _MINIO_LDAP_TEST_SERVER: ${{ matrix.ldap }}
- run: |
- make test-iam-ldap-upgrade-import
- - name: Test LDAP for automatic site replication
- if: matrix.ldap == 'localhost:389'
- run: |
- make test-site-replication-ldap
- - name: Test OIDC for automatic site replication
- if: matrix.openid == 'http://127.0.0.1:5556/dex'
- run: |
- make test-site-replication-oidc
- iam-import-with-missing-entities:
- name: Test IAM import in new cluster with missing entities
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Checkout minio-iam-testing
- uses: actions/checkout@v4
- with:
- repository: minio/minio-iam-testing
- path: minio-iam-testing
- - name: Test import of IAM artifacts when in fresh cluster there are missing groups etc
- run: |
- make test-iam-import-with-missing-entities
- iam-import-with-openid:
- name: Test IAM import in new cluster with opendid configurations
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Checkout minio-iam-testing
- uses: actions/checkout@v4
- with:
- repository: minio/minio-iam-testing
- path: minio-iam-testing
- - name: Test import of IAM artifacts when in fresh cluster with openid configurations
- run: |
- make test-iam-import-with-openid
diff --git a/.github/workflows/issues.yaml b/.github/workflows/issues.yaml
deleted file mode 100644
index d91950aeb2b77..0000000000000
--- a/.github/workflows/issues.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-# @format
-
-name: Issue Workflow
-
-on:
- issues:
- types:
- - opened
-
-jobs:
- add-to-project:
- name: Add issue to project
- runs-on: ubuntu-latest
- steps:
- - uses: actions/add-to-project@v0.5.0
- with:
- project-url: https://github.com/orgs/miniohq/projects/2
- github-token: ${{ secrets.BOT_PAT }}
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
deleted file mode 100644
index da3afe3c4627e..0000000000000
--- a/.github/workflows/lock.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-name: 'Lock Threads'
-
-on:
- schedule:
- - cron: '0 0 * * *'
- workflow_dispatch:
-
-permissions:
- issues: write
-
-concurrency:
- group: lock
-
-jobs:
- action:
- runs-on: ubuntu-latest
- steps:
- - uses: dessant/lock-threads@v3
- with:
- github-token: ${{ github.token }}
- issue-inactive-days: '365'
- exclude-any-issue-labels: 'do-not-close'
- issue-lock-reason: 'resolved'
- log-output: true
diff --git a/.github/workflows/mint.yml b/.github/workflows/mint.yml
deleted file mode 100644
index 77dc498cec39e..0000000000000
--- a/.github/workflows/mint.yml
+++ /dev/null
@@ -1,81 +0,0 @@
-name: Mint Tests
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- mint-test:
- runs-on: mint
- timeout-minutes: 120
- steps:
- - name: cleanup #https://github.com/actions/checkout/issues/273
- run: |
- sudo -S rm -rf ${GITHUB_WORKSPACE}
- mkdir ${GITHUB_WORKSPACE}
- - name: checkout-step
- uses: actions/checkout@v4
-
- - name: setup-go-step
- uses: actions/setup-go@v5
- with:
- go-version: 1.24.x
-
- - name: github sha short
- id: vars
- run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-
- - name: build-minio
- run: |
- TAG="quay.io/minio/minio:${{ steps.vars.outputs.sha_short }}" make docker
-
- - name: multipart uploads test
- run: |
- ${GITHUB_WORKSPACE}/.github/workflows/multipart/migrate.sh "${{ steps.vars.outputs.sha_short }}"
-
- - name: compress and encrypt
- run: |
- ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "compress-encrypt" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
-
- - name: multiple pools
- run: |
- ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "pools" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
-
- - name: standalone erasure
- run: |
- ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "erasure" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
-
- # FIXME: renable this back when we have a valid way to add deadlines for PUT()s (internode CreateFile)
- # - name: resiliency
- # run: |
- # ${GITHUB_WORKSPACE}/.github/workflows/run-mint.sh "resiliency" "minio" "minio123" "${{ steps.vars.outputs.sha_short }}"
-
- - name: The job must cleanup
- if: ${{ always() }}
- run: |
- export JOB_NAME=${{ steps.vars.outputs.sha_short }}
- for mode in $(echo compress-encrypt pools erasure); do
- docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml down || true
- docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/mint/minio-${mode}.yaml rm || true
- done
-
- docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/multipart/docker-compose-site1.yaml rm -s -f || true
- docker-compose -f ${GITHUB_WORKSPACE}/.github/workflows/multipart/docker-compose-site2.yaml rm -s -f || true
- for volume in $(docker volume ls -q | grep minio); do
- docker volume rm ${volume} || true
- done
-
- docker rmi -f quay.io/minio/minio:${{ steps.vars.outputs.sha_short }}
- docker system prune -f || true
- docker volume prune -f || true
- docker volume rm $(docker volume ls -q -f dangling=true) || true
diff --git a/.github/workflows/mint/minio-compress-encrypt.yaml b/.github/workflows/mint/minio-compress-encrypt.yaml
deleted file mode 100644
index fe1238e80d3d8..0000000000000
--- a/.github/workflows/mint/minio-compress-encrypt.yaml
+++ /dev/null
@@ -1,80 +0,0 @@
-version: '3.7'
-
-# Settings and configurations that are common for all containers
-x-minio-common: &minio-common
- image: quay.io/minio/minio:${JOB_NAME}
- command: server --console-address ":9001" http://minio{1...4}/cdata{1...2}
- expose:
- - "9000"
- - "9001"
- environment:
- MINIO_CI_CD: "on"
- MINIO_ROOT_USER: "minio"
- MINIO_ROOT_PASSWORD: "minio123"
- MINIO_COMPRESSION_ENABLE: "on"
- MINIO_COMPRESSION_MIME_TYPES: "*"
- MINIO_COMPRESSION_ALLOW_ENCRYPTION: "on"
- MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
- healthcheck:
- test: ["CMD", "mc", "ready", "local"]
- interval: 5s
- timeout: 5s
- retries: 5
-
-# starts 4 docker containers running minio server instances.
-# using nginx reverse proxy, load balancing, you can access
-# it through port 9000.
-services:
- minio1:
- <<: *minio-common
- hostname: minio1
- volumes:
- - cdata1-1:/cdata1
- - cdata1-2:/cdata2
-
- minio2:
- <<: *minio-common
- hostname: minio2
- volumes:
- - cdata2-1:/cdata1
- - cdata2-2:/cdata2
-
- minio3:
- <<: *minio-common
- hostname: minio3
- volumes:
- - cdata3-1:/cdata1
- - cdata3-2:/cdata2
-
- minio4:
- <<: *minio-common
- hostname: minio4
- volumes:
- - cdata4-1:/cdata1
- - cdata4-2:/cdata2
-
- nginx:
- image: nginx:1.19.2-alpine
- hostname: nginx
- volumes:
- - ./nginx-4-node.conf:/etc/nginx/nginx.conf:ro
- ports:
- - "9000:9000"
- - "9001:9001"
- depends_on:
- - minio1
- - minio2
- - minio3
- - minio4
-
-## By default this config uses default local driver,
-## For custom volumes replace with volume driver configuration.
-volumes:
- cdata1-1:
- cdata1-2:
- cdata2-1:
- cdata2-2:
- cdata3-1:
- cdata3-2:
- cdata4-1:
- cdata4-2:
diff --git a/.github/workflows/mint/minio-erasure.yaml b/.github/workflows/mint/minio-erasure.yaml
deleted file mode 100644
index a3fd6dbc0de21..0000000000000
--- a/.github/workflows/mint/minio-erasure.yaml
+++ /dev/null
@@ -1,51 +0,0 @@
-version: '3.7'
-
-# Settings and configurations that are common for all containers
-x-minio-common: &minio-common
- image: quay.io/minio/minio:${JOB_NAME}
- command: server --console-address ":9001" edata{1...4}
- expose:
- - "9000"
- - "9001"
- environment:
- MINIO_CI_CD: "on"
- MINIO_ROOT_USER: "minio"
- MINIO_ROOT_PASSWORD: "minio123"
- MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
- healthcheck:
- test: ["CMD", "mc", "ready", "local"]
- interval: 5s
- timeout: 5s
- retries: 5
-
-# starts 4 docker containers running minio server instances.
-# using nginx reverse proxy, load balancing, you can access
-# it through port 9000.
-services:
- minio1:
- <<: *minio-common
- hostname: minio1
- volumes:
- - edata1-1:/edata1
- - edata1-2:/edata2
- - edata1-3:/edata3
- - edata1-4:/edata4
-
- nginx:
- image: nginx:1.19.2-alpine
- hostname: nginx
- volumes:
- - ./nginx-1-node.conf:/etc/nginx/nginx.conf:ro
- ports:
- - "9000:9000"
- - "9001:9001"
- depends_on:
- - minio1
-
-## By default this config uses default local driver,
-## For custom volumes replace with volume driver configuration.
-volumes:
- edata1-1:
- edata1-2:
- edata1-3:
- edata1-4:
diff --git a/.github/workflows/mint/minio-pools.yaml b/.github/workflows/mint/minio-pools.yaml
deleted file mode 100644
index bd79fdae048e6..0000000000000
--- a/.github/workflows/mint/minio-pools.yaml
+++ /dev/null
@@ -1,117 +0,0 @@
-version: '3.7'
-
-# Settings and configurations that are common for all containers
-x-minio-common: &minio-common
- image: quay.io/minio/minio:${JOB_NAME}
- command: server --console-address ":9001" http://minio{1...4}/pdata{1...2} http://minio{5...8}/pdata{1...2}
- expose:
- - "9000"
- - "9001"
- environment:
- MINIO_CI_CD: "on"
- MINIO_ROOT_USER: "minio"
- MINIO_ROOT_PASSWORD: "minio123"
- MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
- healthcheck:
- test: ["CMD", "mc", "ready", "local"]
- interval: 5s
- timeout: 5s
- retries: 5
-
-# starts 4 docker containers running minio server instances.
-# using nginx reverse proxy, load balancing, you can access
-# it through port 9000.
-services:
- minio1:
- <<: *minio-common
- hostname: minio1
- volumes:
- - pdata1-1:/pdata1
- - pdata1-2:/pdata2
-
- minio2:
- <<: *minio-common
- hostname: minio2
- volumes:
- - pdata2-1:/pdata1
- - pdata2-2:/pdata2
-
- minio3:
- <<: *minio-common
- hostname: minio3
- volumes:
- - pdata3-1:/pdata1
- - pdata3-2:/pdata2
-
- minio4:
- <<: *minio-common
- hostname: minio4
- volumes:
- - pdata4-1:/pdata1
- - pdata4-2:/pdata2
-
- minio5:
- <<: *minio-common
- hostname: minio5
- volumes:
- - pdata5-1:/pdata1
- - pdata5-2:/pdata2
-
- minio6:
- <<: *minio-common
- hostname: minio6
- volumes:
- - pdata6-1:/pdata1
- - pdata6-2:/pdata2
-
- minio7:
- <<: *minio-common
- hostname: minio7
- volumes:
- - pdata7-1:/pdata1
- - pdata7-2:/pdata2
-
- minio8:
- <<: *minio-common
- hostname: minio8
- volumes:
- - pdata8-1:/pdata1
- - pdata8-2:/pdata2
-
- nginx:
- image: nginx:1.19.2-alpine
- hostname: nginx
- volumes:
- - ./nginx-8-node.conf:/etc/nginx/nginx.conf:ro
- ports:
- - "9000:9000"
- - "9001:9001"
- depends_on:
- - minio1
- - minio2
- - minio3
- - minio4
- - minio5
- - minio6
- - minio7
- - minio8
-
-## By default this config uses default local driver,
-## For custom volumes replace with volume driver configuration.
-volumes:
- pdata1-1:
- pdata1-2:
- pdata2-1:
- pdata2-2:
- pdata3-1:
- pdata3-2:
- pdata4-1:
- pdata4-2:
- pdata5-1:
- pdata5-2:
- pdata6-1:
- pdata6-2:
- pdata7-1:
- pdata7-2:
- pdata8-1:
- pdata8-2:
diff --git a/.github/workflows/mint/minio-resiliency.yaml b/.github/workflows/mint/minio-resiliency.yaml
deleted file mode 100644
index 9d569c59e4cff..0000000000000
--- a/.github/workflows/mint/minio-resiliency.yaml
+++ /dev/null
@@ -1,78 +0,0 @@
-version: '3.7'
-
-# Settings and configurations that are common for all containers
-x-minio-common: &minio-common
- image: quay.io/minio/minio:${JOB_NAME}
- command: server --console-address ":9001" http://minio{1...4}/rdata{1...2}
- expose:
- - "9000"
- - "9001"
- environment:
- MINIO_CI_CD: "on"
- MINIO_ROOT_USER: "minio"
- MINIO_ROOT_PASSWORD: "minio123"
- MINIO_KMS_SECRET_KEY: "my-minio-key:OSMM+vkKUTCvQs9YL/CVMIMt43HFhkUpqJxTmGl6rYw="
- MINIO_DRIVE_MAX_TIMEOUT: "5s"
- healthcheck:
- test: ["CMD", "mc", "ready", "local"]
- interval: 5s
- timeout: 5s
- retries: 5
-
-# starts 4 docker containers running minio server instances.
-# using nginx reverse proxy, load balancing, you can access
-# it through port 9000.
-services:
- minio1:
- <<: *minio-common
- hostname: minio1
- volumes:
- - rdata1-1:/rdata1
- - rdata1-2:/rdata2
-
- minio2:
- <<: *minio-common
- hostname: minio2
- volumes:
- - rdata2-1:/rdata1
- - rdata2-2:/rdata2
-
- minio3:
- <<: *minio-common
- hostname: minio3
- volumes:
- - rdata3-1:/rdata1
- - rdata3-2:/rdata2
-
- minio4:
- <<: *minio-common
- hostname: minio4
- volumes:
- - rdata4-1:/rdata1
- - rdata4-2:/rdata2
-
- nginx:
- image: nginx:1.19.2-alpine
- hostname: nginx
- volumes:
- - ./nginx-4-node.conf:/etc/nginx/nginx.conf:ro
- ports:
- - "9000:9000"
- - "9001:9001"
- depends_on:
- - minio1
- - minio2
- - minio3
- - minio4
-
-## By default this config uses default local driver,
-## For custom volumes replace with volume driver configuration.
-volumes:
- rdata1-1:
- rdata1-2:
- rdata2-1:
- rdata2-2:
- rdata3-1:
- rdata3-2:
- rdata4-1:
- rdata4-2:
diff --git a/.github/workflows/mint/nginx-1-node.conf b/.github/workflows/mint/nginx-1-node.conf
deleted file mode 100644
index bb13bde24d71c..0000000000000
--- a/.github/workflows/mint/nginx-1-node.conf
+++ /dev/null
@@ -1,100 +0,0 @@
-user nginx;
-worker_processes auto;
-
-error_log /var/log/nginx/error.log warn;
-pid /var/run/nginx.pid;
-
-events {
- worker_connections 4096;
-}
-
-http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
-
- access_log /var/log/nginx/access.log main;
- sendfile on;
- keepalive_timeout 65;
-
- # include /etc/nginx/conf.d/*.conf;
-
- upstream minio {
- server minio1:9000;
- }
-
- upstream console {
- ip_hash;
- server minio1:9001;
- }
-
- server {
- listen 9000;
- listen [::]:9000;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- proxy_connect_timeout 300;
- # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- chunked_transfer_encoding off;
-
- proxy_pass http://minio;
- }
- }
-
- server {
- listen 9001;
- listen [::]:9001;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-NginX-Proxy true;
-
- # This is necessary to pass the correct IP to be hashed
- real_ip_header X-Real-IP;
-
- proxy_connect_timeout 300;
-
- # To support websocket
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
-
- chunked_transfer_encoding off;
-
- proxy_pass http://console;
- }
- }
-}
diff --git a/.github/workflows/mint/nginx-4-node.conf b/.github/workflows/mint/nginx-4-node.conf
deleted file mode 100644
index b849940d972a9..0000000000000
--- a/.github/workflows/mint/nginx-4-node.conf
+++ /dev/null
@@ -1,105 +0,0 @@
-user nginx;
-worker_processes auto;
-
-error_log /var/log/nginx/error.log warn;
-pid /var/run/nginx.pid;
-
-events {
- worker_connections 4096;
-}
-
-http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
-
- access_log /var/log/nginx/access.log main;
- sendfile on;
- keepalive_timeout 65;
-
- # include /etc/nginx/conf.d/*.conf;
-
- upstream minio {
- server minio1:9000 max_fails=1 fail_timeout=10s;
- server minio2:9000 max_fails=1 fail_timeout=10s;
- server minio3:9000 max_fails=1 fail_timeout=10s;
- }
-
- upstream console {
- ip_hash;
- server minio1:9001;
- server minio2:9001;
- server minio3:9001;
- server minio4:9001;
- }
-
- server {
- listen 9000;
- listen [::]:9000;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- proxy_connect_timeout 300;
- # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- chunked_transfer_encoding off;
-
- proxy_pass http://minio;
- }
- }
-
- server {
- listen 9001;
- listen [::]:9001;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-NginX-Proxy true;
-
- # This is necessary to pass the correct IP to be hashed
- real_ip_header X-Real-IP;
-
- proxy_connect_timeout 300;
-
- # To support websocket
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
-
- chunked_transfer_encoding off;
-
- proxy_pass http://console;
- }
- }
-}
diff --git a/.github/workflows/mint/nginx-8-node.conf b/.github/workflows/mint/nginx-8-node.conf
deleted file mode 100644
index 278b5ae76e6c2..0000000000000
--- a/.github/workflows/mint/nginx-8-node.conf
+++ /dev/null
@@ -1,114 +0,0 @@
-user nginx;
-worker_processes auto;
-
-error_log /var/log/nginx/error.log warn;
-pid /var/run/nginx.pid;
-
-events {
- worker_connections 4096;
-}
-
-http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
-
- access_log /var/log/nginx/access.log main;
- sendfile on;
- keepalive_timeout 65;
-
- # include /etc/nginx/conf.d/*.conf;
-
- upstream minio {
- server minio1:9000 max_fails=1 fail_timeout=10s;
- server minio2:9000 max_fails=1 fail_timeout=10s;
- server minio3:9000 max_fails=1 fail_timeout=10s;
- server minio4:9000 max_fails=1 fail_timeout=10s;
- server minio5:9000 max_fails=1 fail_timeout=10s;
- server minio6:9000 max_fails=1 fail_timeout=10s;
- server minio7:9000 max_fails=1 fail_timeout=10s;
- server minio8:9000 max_fails=1 fail_timeout=10s;
- }
-
- upstream console {
- ip_hash;
- server minio1:9001;
- server minio2:9001;
- server minio3:9001;
- server minio4:9001;
- server minio5:9001;
- server minio6:9001;
- server minio7:9001;
- server minio8:9001;
- }
-
- server {
- listen 9000;
- listen [::]:9000;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- proxy_connect_timeout 300;
- # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- chunked_transfer_encoding off;
-
- proxy_pass http://minio;
- }
- }
-
- server {
- listen 9001;
- listen [::]:9001;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-NginX-Proxy true;
-
- # This is necessary to pass the correct IP to be hashed
- real_ip_header X-Real-IP;
-
- proxy_connect_timeout 300;
-
- # To support websocket
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
-
- chunked_transfer_encoding off;
-
- proxy_pass http://console;
- }
- }
-}
diff --git a/.github/workflows/mint/nginx.conf b/.github/workflows/mint/nginx.conf
deleted file mode 100644
index 1455a3e2f13b3..0000000000000
--- a/.github/workflows/mint/nginx.conf
+++ /dev/null
@@ -1,106 +0,0 @@
-user nginx;
-worker_processes auto;
-
-error_log /var/log/nginx/error.log warn;
-pid /var/run/nginx.pid;
-
-events {
- worker_connections 4096;
-}
-
-http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
-
- access_log /var/log/nginx/access.log main;
- sendfile on;
- keepalive_timeout 65;
-
- # include /etc/nginx/conf.d/*.conf;
-
- upstream minio {
- server minio1:9000 max_fails=1 fail_timeout=10s;
- server minio2:9000 max_fails=1 fail_timeout=10s;
- server minio3:9000 max_fails=1 fail_timeout=10s;
- server minio4:9000 max_fails=1 fail_timeout=10s;
- }
-
- upstream console {
- ip_hash;
- server minio1:9001;
- server minio2:9001;
- server minio3:9001;
- server minio4:9001;
- }
-
- server {
- listen 9000;
- listen [::]:9000;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- proxy_connect_timeout 300;
- # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- chunked_transfer_encoding off;
-
- proxy_pass http://minio;
- }
- }
-
- server {
- listen 9001;
- listen [::]:9001;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-NginX-Proxy true;
-
- # This is necessary to pass the correct IP to be hashed
- real_ip_header X-Real-IP;
-
- proxy_connect_timeout 300;
-
- # To support websocket
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
-
- chunked_transfer_encoding off;
-
- proxy_pass http://console;
- }
- }
-}
diff --git a/.github/workflows/multipart/docker-compose-site1.yaml b/.github/workflows/multipart/docker-compose-site1.yaml
deleted file mode 100644
index b4a665ef068b1..0000000000000
--- a/.github/workflows/multipart/docker-compose-site1.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-version: '3.7'
-
-# Settings and configurations that are common for all containers
-x-minio-common: &minio-common
- image: quay.io/minio/minio:${RELEASE}
- command: server http://site1-minio{1...4}/data{1...2}
- environment:
- - MINIO_PROMETHEUS_AUTH_TYPE=public
- - CI=true
-
-# starts 4 docker containers running minio server instances.
-# using nginx reverse proxy, load balancing, you can access
-# it through port 9000.
-services:
- site1-minio1:
- <<: *minio-common
- hostname: site1-minio1
- volumes:
- - site1-data1-1:/data1
- - site1-data1-2:/data2
-
- site1-minio2:
- <<: *minio-common
- hostname: site1-minio2
- volumes:
- - site1-data2-1:/data1
- - site1-data2-2:/data2
-
- site1-minio3:
- <<: *minio-common
- hostname: site1-minio3
- volumes:
- - site1-data3-1:/data1
- - site1-data3-2:/data2
-
- site1-minio4:
- <<: *minio-common
- hostname: site1-minio4
- volumes:
- - site1-data4-1:/data1
- - site1-data4-2:/data2
-
- site1-nginx:
- image: nginx:1.19.2-alpine
- hostname: site1-nginx
- volumes:
- - ./nginx-site1.conf:/etc/nginx/nginx.conf:ro
- ports:
- - "9001:9001"
- depends_on:
- - site1-minio1
- - site1-minio2
- - site1-minio3
- - site1-minio4
-
-## By default this config uses default local driver,
-## For custom volumes replace with volume driver configuration.
-volumes:
- site1-data1-1:
- site1-data1-2:
- site1-data2-1:
- site1-data2-2:
- site1-data3-1:
- site1-data3-2:
- site1-data4-1:
- site1-data4-2:
diff --git a/.github/workflows/multipart/docker-compose-site2.yaml b/.github/workflows/multipart/docker-compose-site2.yaml
deleted file mode 100644
index 31a1f7a8c9e41..0000000000000
--- a/.github/workflows/multipart/docker-compose-site2.yaml
+++ /dev/null
@@ -1,66 +0,0 @@
-version: '3.7'
-
-# Settings and configurations that are common for all containers
-x-minio-common: &minio-common
- image: quay.io/minio/minio:${RELEASE}
- command: server http://site2-minio{1...4}/data{1...2}
- environment:
- - MINIO_PROMETHEUS_AUTH_TYPE=public
- - CI=true
-
-# starts 4 docker containers running minio server instances.
-# using nginx reverse proxy, load balancing, you can access
-# it through port 9000.
-services:
- site2-minio1:
- <<: *minio-common
- hostname: site2-minio1
- volumes:
- - site2-data1-1:/data1
- - site2-data1-2:/data2
-
- site2-minio2:
- <<: *minio-common
- hostname: site2-minio2
- volumes:
- - site2-data2-1:/data1
- - site2-data2-2:/data2
-
- site2-minio3:
- <<: *minio-common
- hostname: site2-minio3
- volumes:
- - site2-data3-1:/data1
- - site2-data3-2:/data2
-
- site2-minio4:
- <<: *minio-common
- hostname: site2-minio4
- volumes:
- - site2-data4-1:/data1
- - site2-data4-2:/data2
-
- site2-nginx:
- image: nginx:1.19.2-alpine
- hostname: site2-nginx
- volumes:
- - ./nginx-site2.conf:/etc/nginx/nginx.conf:ro
- ports:
- - "9002:9002"
- depends_on:
- - site2-minio1
- - site2-minio2
- - site2-minio3
- - site2-minio4
-
-## By default this config uses default local driver,
-## For custom volumes replace with volume driver configuration.
-volumes:
- site2-data1-1:
- site2-data1-2:
- site2-data2-1:
- site2-data2-2:
- site2-data3-1:
- site2-data3-2:
- site2-data4-1:
- site2-data4-2:
diff --git a/.github/workflows/multipart/migrate.sh b/.github/workflows/multipart/migrate.sh
deleted file mode 100755
index 6058b008c8836..0000000000000
--- a/.github/workflows/multipart/migrate.sh
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/bin/bash
-
-set -x
-
-## change working directory
-cd .github/workflows/multipart/
-
-function cleanup() {
- docker-compose -f docker-compose-site1.yaml rm -s -f || true
- docker-compose -f docker-compose-site2.yaml rm -s -f || true
- for volume in $(docker volume ls -q | grep minio); do
- docker volume rm ${volume} || true
- done
-
- docker system prune -f || true
- docker volume prune -f || true
- docker volume rm $(docker volume ls -q -f dangling=true) || true
-}
-
-cleanup
-
-if [ ! -f ./mc ]; then
- wget --quiet -O mc https://dl.minio.io/client/mc/release/linux-amd64/mc &&
- chmod +x mc
-fi
-
-export RELEASE=RELEASE.2023-08-29T23-07-35Z
-
-docker-compose -f docker-compose-site1.yaml up -d
-docker-compose -f docker-compose-site2.yaml up -d
-
-sleep 30s
-
-./mc alias set site1 http://site1-nginx:9001 minioadmin minioadmin --api s3v4
-./mc alias set site2 http://site2-nginx:9002 minioadmin minioadmin --api s3v4
-
-./mc ready site1/
-./mc ready site2/
-
-./mc admin replicate add site1 site2
-./mc mb site1/testbucket/
-./mc cp -r --quiet /usr/bin site1/testbucket/
-
-sleep 5
-
-./s3-check-md5 -h
-
-failed_count_site1=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l)
-failed_count_site2=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l)
-
-if [ $failed_count_site1 -ne 0 ]; then
- echo "failed with multipart on site1 uploads"
- exit 1
-fi
-
-if [ $failed_count_site2 -ne 0 ]; then
- echo "failed with multipart on site2 uploads"
- exit 1
-fi
-
-./mc cp -r --quiet /usr/bin site1/testbucket/
-
-sleep 5
-
-failed_count_site1=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l)
-failed_count_site2=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l)
-
-## we do not need to fail here, since we are going to test
-## upgrading to master, healing and being able to recover
-## the last version.
-if [ $failed_count_site1 -ne 0 ]; then
- echo "failed with multipart on site1 uploads ${failed_count_site1}"
-fi
-
-if [ $failed_count_site2 -ne 0 ]; then
- echo "failed with multipart on site2 uploads ${failed_count_site2}"
-fi
-
-export RELEASE=${1}
-
-docker-compose -f docker-compose-site1.yaml up -d
-docker-compose -f docker-compose-site2.yaml up -d
-
-./mc ready site1/
-./mc ready site2/
-
-for i in $(seq 1 10); do
- # mc admin heal -r --remove when used against a LB endpoint
- # behaves flaky, let this run 10 times before giving up
- ./mc admin heal -r --remove --json site1/ 2>&1 >/dev/null
- ./mc admin heal -r --remove --json site2/ 2>&1 >/dev/null
-done
-
-failed_count_site1=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site1-nginx:9001 -bucket testbucket 2>&1 | grep FAILED | wc -l)
-failed_count_site2=$(./s3-check-md5 -versions -access-key minioadmin -secret-key minioadmin -endpoint http://site2-nginx:9002 -bucket testbucket 2>&1 | grep FAILED | wc -l)
-
-if [ $failed_count_site1 -ne 0 ]; then
- echo "failed with multipart on site1 uploads"
- exit 1
-fi
-
-if [ $failed_count_site2 -ne 0 ]; then
- echo "failed with multipart on site2 uploads"
- exit 1
-fi
-
-# Add user group test
-./mc admin user add site1 site-replication-issue-user site-replication-issue-password
-./mc admin group add site1 site-replication-issue-group site-replication-issue-user
-
-max_wait_attempts=30
-wait_interval=5
-
-attempt=1
-while true; do
- diff <(./mc admin group info site1 site-replication-issue-group) <(./mc admin group info site2 site-replication-issue-group)
-
- if [[ $? -eq 0 ]]; then
- echo "Outputs are consistent."
- break
- fi
-
- remaining_attempts=$((max_wait_attempts - attempt))
- if ((attempt >= max_wait_attempts)); then
- echo "Outputs remain inconsistent after $max_wait_attempts attempts. Exiting with error."
- exit 1
- else
- echo "Outputs are inconsistent. Waiting for $wait_interval seconds (attempt $attempt/$max_wait_attempts)."
- sleep $wait_interval
- fi
-
- ((attempt++))
-done
-
-status=$(./mc admin group info site1 site-replication-issue-group --json | jq .groupStatus | tr -d '"')
-
-if [[ $status == "enabled" ]]; then
- echo "Success"
-else
- echo "Expected status: enabled, actual status: $status"
- exit 1
-fi
-
-cleanup
-
-## change working directory
-cd ../../../
diff --git a/.github/workflows/multipart/nginx-site1.conf b/.github/workflows/multipart/nginx-site1.conf
deleted file mode 100644
index 1a37b9d2bc964..0000000000000
--- a/.github/workflows/multipart/nginx-site1.conf
+++ /dev/null
@@ -1,61 +0,0 @@
-user nginx;
-worker_processes auto;
-
-error_log /var/log/nginx/error.log warn;
-pid /var/run/nginx.pid;
-
-events {
- worker_connections 4096;
-}
-
-http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
-
- access_log /var/log/nginx/access.log main;
- sendfile on;
- keepalive_timeout 65;
-
- # include /etc/nginx/conf.d/*.conf;
-
- upstream minio {
- server site1-minio1:9000;
- server site1-minio2:9000;
- server site1-minio3:9000;
- server site1-minio4:9000;
- }
-
- server {
- listen 9001;
- listen [::]:9001;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- proxy_connect_timeout 300;
- # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- chunked_transfer_encoding off;
-
- proxy_pass http://minio;
- }
- }
-}
diff --git a/.github/workflows/multipart/nginx-site2.conf b/.github/workflows/multipart/nginx-site2.conf
deleted file mode 100644
index b93bba24d6135..0000000000000
--- a/.github/workflows/multipart/nginx-site2.conf
+++ /dev/null
@@ -1,61 +0,0 @@
-user nginx;
-worker_processes auto;
-
-error_log /var/log/nginx/error.log warn;
-pid /var/run/nginx.pid;
-
-events {
- worker_connections 4096;
-}
-
-http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
-
- access_log /var/log/nginx/access.log main;
- sendfile on;
- keepalive_timeout 65;
-
- # include /etc/nginx/conf.d/*.conf;
-
- upstream minio {
- server site2-minio1:9000;
- server site2-minio2:9000;
- server site2-minio3:9000;
- server site2-minio4:9000;
- }
-
- server {
- listen 9002;
- listen [::]:9002;
- server_name localhost;
-
- # To allow special characters in headers
- ignore_invalid_headers off;
- # Allow any size file to be uploaded.
- # Set to a value such as 1000m; to restrict file size to a specific value
- client_max_body_size 0;
- # To disable buffering
- proxy_buffering off;
- proxy_request_buffering off;
-
- location / {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- proxy_connect_timeout 300;
- # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
- proxy_http_version 1.1;
- proxy_set_header Connection "";
- chunked_transfer_encoding off;
-
- proxy_pass http://minio;
- }
- }
-}
diff --git a/.github/workflows/replication.yaml b/.github/workflows/replication.yaml
deleted file mode 100644
index 18c3277496b7b..0000000000000
--- a/.github/workflows/replication.yaml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: MinIO advanced tests
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- replication-test:
- name: Advanced Tests with Go ${{ matrix.go-version }}
- runs-on: ubuntu-latest
-
- strategy:
- matrix:
- go-version: [1.24.x]
-
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Test Decom
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-decom
-
- - name: Test ILM
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-ilm
- make test-ilm-transition
-
- - name: Test PBAC
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-pbac
-
- - name: Test Config File
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-configfile
-
- - name: Test Replication
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-replication
-
- - name: Test MinIO IDP for automatic site replication
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-site-replication-minio
-
- - name: Test Versioning
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-versioning
-
- - name: Test Multipart upload with failures
- run: |
- sudo sysctl net.ipv6.conf.all.disable_ipv6=0
- sudo sysctl net.ipv6.conf.default.disable_ipv6=0
- make test-multipart
diff --git a/.github/workflows/root-disable.yml b/.github/workflows/root-disable.yml
deleted file mode 100644
index c08fb8b1fbd9e..0000000000000
--- a/.github/workflows/root-disable.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Root lockdown tests
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- build:
- name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- go-version: [1.24.x]
- os: [ubuntu-latest]
-
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Start root lockdown tests
- run: |
- make test-root-disable
diff --git a/.github/workflows/root.cert b/.github/workflows/root.cert
deleted file mode 100644
index 5f220f79bd429..0000000000000
--- a/.github/workflows/root.cert
+++ /dev/null
@@ -1,9 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIBKDCB26ADAgECAhB6vebGMUfKnmBKyqoApRSOMAUGAytlcDAbMRkwFwYDVQQD
-DBByb290QHBsYXkubWluLmlvMB4XDTIwMDQzMDE1MjIyNVoXDTI1MDQyOTE1MjIy
-NVowGzEZMBcGA1UEAwwQcm9vdEBwbGF5Lm1pbi5pbzAqMAUGAytlcAMhALzn735W
-fmSH/ghKs+4iPWziZMmWdiWr/sqvqeW+WwSxozUwMzAOBgNVHQ8BAf8EBAMCB4Aw
-EwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAFBgMrZXADQQDZOrGK
-b2ATkDlu2pTcP3LyhSBDpYh7V4TvjRkBTRgjkacCzwFLm+mh+7US8V4dBpIDsJ4u
-uWoF0y6vbLVGIlkG
------END CERTIFICATE-----
diff --git a/.github/workflows/root.key b/.github/workflows/root.key
deleted file mode 100644
index 53a47e25da515..0000000000000
--- a/.github/workflows/root.key
+++ /dev/null
@@ -1,3 +0,0 @@
------BEGIN PRIVATE KEY-----
-MC4CAQAwBQYDK2VwBCIEID9E7FSYWrMD+VjhI6q545cYT9YOyFxZb7UnjEepYDRc
------END PRIVATE KEY-----
diff --git a/.github/workflows/run-mint.sh b/.github/workflows/run-mint.sh
deleted file mode 100755
index 0bbc1cbaf6150..0000000000000
--- a/.github/workflows/run-mint.sh
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/bin/bash
-
-set -ex
-
-export MODE="$1"
-export ACCESS_KEY="$2"
-export SECRET_KEY="$3"
-export JOB_NAME="$4"
-export MINT_MODE="full"
-
-docker system prune -f || true
-docker volume prune -f || true
-docker volume rm $(docker volume ls -f dangling=true) || true
-
-## change working directory
-cd .github/workflows/mint
-
-## always pull latest
-docker pull docker.io/minio/mint:edge
-
-docker-compose -f minio-${MODE}.yaml up -d
-sleep 1m
-
-docker system prune -f || true
-docker volume prune -f || true
-docker volume rm $(docker volume ls -q -f dangling=true) || true
-
-# Stop two nodes, one of each pool, to check that all S3 calls work while quorum is still there
-[ "${MODE}" == "pools" ] && docker-compose -f minio-${MODE}.yaml stop minio2
-[ "${MODE}" == "pools" ] && docker-compose -f minio-${MODE}.yaml stop minio6
-
-# Pause one node, to check that all S3 calls work while one node goes wrong
-[ "${MODE}" == "resiliency" ] && docker-compose -f minio-${MODE}.yaml pause minio4
-
-docker run --rm --net=mint_default \
- --name="mint-${MODE}-${JOB_NAME}" \
- -e SERVER_ENDPOINT="nginx:9000" \
- -e ACCESS_KEY="${ACCESS_KEY}" \
- -e SECRET_KEY="${SECRET_KEY}" \
- -e ENABLE_HTTPS=0 \
- -e MINT_MODE="${MINT_MODE}" \
- docker.io/minio/mint:edge
-
-# FIXME: enable this after fixing aws-sdk-java-v2 tests
-# # unpause the node, to check that all S3 calls work while one node goes wrong
-# [ "${MODE}" == "resiliency" ] && docker-compose -f minio-${MODE}.yaml unpause minio4
-# [ "${MODE}" == "resiliency" ] && docker run --rm --net=mint_default \
-# --name="mint-${MODE}-${JOB_NAME}" \
-# -e SERVER_ENDPOINT="nginx:9000" \
-# -e ACCESS_KEY="${ACCESS_KEY}" \
-# -e SECRET_KEY="${SECRET_KEY}" \
-# -e ENABLE_HTTPS=0 \
-# -e MINT_MODE="${MINT_MODE}" \
-# docker.io/minio/mint:edge
-
-docker-compose -f minio-${MODE}.yaml down || true
-sleep 10s
-
-docker system prune -f || true
-docker volume prune -f || true
-docker volume rm $(docker volume ls -q -f dangling=true) || true
-
-## change working directory
-cd ../../../
diff --git a/.github/workflows/shfmt.yml b/.github/workflows/shfmt.yml
deleted file mode 100644
index 3e446306bdd55..0000000000000
--- a/.github/workflows/shfmt.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: Shell formatting checks
-
-on:
- pull_request:
- branches:
- - master
-
-permissions:
- contents: read
-
-jobs:
- build:
- name: runner / shfmt
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: luizm/action-sh-checker@master
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SHFMT_OPTS: "-s"
- with:
- sh_checker_shellcheck_disable: true # disable for now
diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml
deleted file mode 100644
index 7addb3143b61c..0000000000000
--- a/.github/workflows/typos.yml
+++ /dev/null
@@ -1,15 +0,0 @@
----
-name: Spelling
-on: [pull_request]
-
-jobs:
- run:
- name: Spell Check with Typos
- runs-on: ubuntu-latest
- steps:
- - name: Checkout Actions Repository
- uses: actions/checkout@v4
-
- - name: Check spelling of repo
- uses: crate-ci/typos@master
-
diff --git a/.github/workflows/upgrade-ci-cd.yaml b/.github/workflows/upgrade-ci-cd.yaml
deleted file mode 100644
index d3e71ff593641..0000000000000
--- a/.github/workflows/upgrade-ci-cd.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Upgrade old version tests
-
-on:
- pull_request:
- branches:
- - master
-
-# This ensures that previous jobs for the PR are canceled when the PR is
-# updated.
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- build:
- name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- go-version: [1.24.x]
- os: [ubuntu-latest]
-
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version: ${{ matrix.go-version }}
- check-latest: true
- - name: Start upgrade tests
- run: |
- make test-upgrade
diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml
deleted file mode 100644
index 5dfcde04bde5e..0000000000000
--- a/.github/workflows/vulncheck.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: VulnCheck
-on:
- pull_request:
- branches:
- - master
-
- push:
- branches:
- - master
-
-permissions:
- contents: read # to fetch code (actions/checkout)
-
-jobs:
- vulncheck:
- name: Analysis
- runs-on: ubuntu-latest
- steps:
- - name: Check out code into the Go module directory
- uses: actions/checkout@v4
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version: 1.24.x
- cached: false
- - name: Get official govulncheck
- run: go install golang.org/x/vuln/cmd/govulncheck@latest
- shell: bash
- - name: Run govulncheck
- run: govulncheck -show verbose ./...
- shell: bash
diff --git a/cmd/auth-handler.go b/cmd/auth-handler.go
index 6e824e3126d46..983195241f0dd 100644
--- a/cmd/auth-handler.go
+++ b/cmd/auth-handler.go
@@ -731,7 +731,11 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
case authTypeStreamingSigned, authTypePresigned, authTypeSigned, authTypeStreamingSignedTrailer:
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
case authTypeStreamingUnsignedTrailer:
- cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
+ // For unsigned trailer streaming the request must either be fully
+ // signed (credentials come from the Authorization header) or be
+ // anonymous. Query-string X-Amz-Credential is not signature-verified
+ // for this auth type and must not be used for authorization.
+ cred, owner, s3Err = getReqAccessKeyV4FromHeader(r, region, serviceS3)
if s3Err == ErrMissingFields {
// Could be anonymous. cred + owner is zero value.
s3Err = ErrNone
diff --git a/cmd/bucket-handlers.go b/cmd/bucket-handlers.go
index 564572f2a94a6..67f3771bd4943 100644
--- a/cmd/bucket-handlers.go
+++ b/cmd/bucket-handlers.go
@@ -1253,8 +1253,10 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
}
// Extract metadata to be saved from received Form.
+ // Post-policy uploads are never replication requests, so replication
+ // SSE headers must not be accepted (CVE-2026-34204).
metadata := make(map[string]string)
- err = extractMetadataFromMime(ctx, textproto.MIMEHeader(formValues), metadata)
+ err = extractMetadataFromMime(ctx, textproto.MIMEHeader(formValues), metadata, false)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
diff --git a/cmd/erasure-decode.go b/cmd/erasure-decode.go
index f0cc90ab09fda..18ec7e9f5b710 100644
--- a/cmd/erasure-decode.go
+++ b/cmd/erasure-decode.go
@@ -24,6 +24,7 @@ import (
"io"
"sync"
"sync/atomic"
+ "time"
xioutil "github.com/minio/minio/internal/ioutil"
)
@@ -39,10 +40,39 @@ type parallelReader struct {
buf [][]byte
readerToBuf []int
stashBuffer []byte
+
+ // Hedged read state. When fanoutDelay is > 0 reads start with
+ // dataBlocks+1 shards in parallel and fan out to all remaining
+ // shards after fanoutDelay.
+ fanoutDelay time.Duration
+ events chan hedgedResult
+ inflight []bool
+ curBlock *hedgedBlock
+ stateLK sync.Mutex
+ missingPartsHeal atomic.Bool
+ bitrotHeal atomic.Bool
+ disksNotFound atomic.Int32
+}
+
+// hedgedBlock tracks per block launch state of a hedged read.
+// All fields are guarded by parallelReader.stateLK.
+type hedgedBlock struct {
+ launched []bool
+ outstanding int
+}
+
+// hedgedResult is the outcome of one launched shard read. blk
+// identifies the block the read was launched for.
+type hedgedResult struct {
+ blk *hedgedBlock
+ i int
+ buf []byte
+ n int
+ err error
}
// newParallelReader returns parallelReader.
-func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int64) *parallelReader {
+func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int64, fanoutDelay time.Duration) *parallelReader {
r2b := make([]int, len(readers))
for i := range r2b {
r2b[i] = i
@@ -51,9 +81,11 @@ func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int
shardSize := int(e.ShardSize())
var b []byte
- // We should always have enough capacity, but older objects may be bigger
- // we do not need stashbuffer for them.
- if globalBytePoolCap.Load().WidthCap() >= len(readers)*shardSize {
+ // When hedging is enabled shard buffers are allocated per reader
+ // on first use instead of seeding from the byte pool: straggler
+ // goroutines may outlive the reader and keep writing into their
+ // buffer after Done() has returned pool memory.
+ if fanoutDelay <= 0 && globalBytePoolCap.Load().WidthCap() >= len(readers)*shardSize {
// Fill buffers
b = globalBytePoolCap.Load().Get()
// Seed the buffers.
@@ -62,7 +94,7 @@ func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int
}
}
- return ¶llelReader{
+ p := ¶llelReader{
readers: readers,
orgReaders: readers,
dataBlocks: e.dataBlocks,
@@ -73,6 +105,12 @@ func newParallelReader(readers []io.ReaderAt, e Erasure, offset, totalLength int
readerToBuf: r2b,
stashBuffer: b,
}
+ if fanoutDelay > 0 {
+ p.fanoutDelay = fanoutDelay
+ p.events = make(chan hedgedResult, len(readers))
+ p.inflight = make([]bool, len(readers))
+ }
+ return p
}
// Done will release any resources used by the parallelReader.
@@ -81,6 +119,18 @@ func (p *parallelReader) Done() {
globalBytePoolCap.Load().Put(p.stashBuffer)
p.stashBuffer = nil
}
+ if p.inflight != nil {
+ // Readers still in flight are stragglers of a returned block.
+ // Nil them in orgReaders so closeBitrotReaders skips them; their
+ // in-flight goroutine closes the reader after the read returns.
+ p.stateLK.Lock()
+ for i := range p.inflight {
+ if p.inflight[i] {
+ p.orgReaders[p.readerToBuf[i]] = nil
+ }
+ }
+ p.stateLK.Unlock()
+ }
}
// preferReaders can mark readers as preferred.
@@ -133,7 +183,6 @@ func (p *parallelReader) Read(dst [][]byte) ([][]byte, error) {
newBuf[i] = newBuf[i][:0]
}
}
- var newBufLK sync.RWMutex
if p.offset+p.shardSize > p.shardFileSize {
p.shardSize = p.shardFileSize - p.offset
@@ -142,6 +191,12 @@ func (p *parallelReader) Read(dst [][]byte) ([][]byte, error) {
return newBuf, nil
}
+ if p.fanoutDelay > 0 {
+ return p.readHedged(newBuf)
+ }
+
+ var newBufLK sync.RWMutex
+
readTriggerCh := make(chan bool, len(p.readers))
defer xioutil.SafeClose(readTriggerCh) // close the channel upon return
@@ -234,6 +289,186 @@ func (p *parallelReader) Read(dst [][]byte) ([][]byte, error) {
return nil, fmt.Errorf("%w (offline-disks=%d/%d)", errErasureReadQuorum, disksNotFound, len(p.readers))
}
+// readHedged implements the hedged read strategy: it launches
+// dataBlocks+1 reads in parallel, substitutes a new shard on error,
+// and fans out to all remaining shards after p.fanoutDelay. It
+// returns as soon as dataBlocks shards have been delivered, without
+// waiting for stragglers. A reader that completes after its block
+// returned re-joins on the next block; a straggler still in flight is
+// never launched twice, so stateful readers are safe.
+func (p *parallelReader) readHedged(newBuf [][]byte) ([][]byte, error) {
+ p.stateLK.Lock()
+
+ // Drain results of stragglers from a previous block, their
+ // readers are available again.
+ for len(p.events) > 0 {
+ <-p.events
+ }
+
+ blk := &hedgedBlock{launched: make([]bool, len(p.readers))}
+ p.curBlock = blk
+
+ // launchNext launches the next launchable reader for this block.
+ // Must be called with p.stateLK held.
+ launchNext := func() bool {
+ for i := range p.readers {
+ if p.readers[i] == nil || p.inflight[i] || blk.launched[i] {
+ continue
+ }
+ blk.launched[i] = true
+ p.inflight[i] = true
+ blk.outstanding++
+ bufIdx := p.readerToBuf[i]
+ if p.buf[bufIdx] == nil {
+ p.buf[bufIdx] = make([]byte, p.shardSize)
+ }
+ buf := p.buf[bufIdx][:p.shardSize]
+ offset := p.offset
+ rr := p.readers[i]
+ go func(i int, bufIdx int, rr io.ReaderAt, buf []byte, offset int64) {
+ n, err := rr.ReadAt(buf, offset)
+ p.stateLK.Lock()
+ p.inflight[i] = false
+ closeReader := false
+ if p.orgReaders[bufIdx] == nil {
+ // Abandoned straggler, marked by Done(); the
+ // caller no longer tracks this reader so it is
+ // closed here instead of by closeBitrotReaders.
+ closeReader = true
+ } else if err != nil {
+ switch {
+ case errors.Is(err, errFileNotFound):
+ p.missingPartsHeal.Store(true)
+ case errors.Is(err, errFileCorrupt):
+ p.bitrotHeal.Store(true)
+ case errors.Is(err, errDiskNotFound):
+ p.disksNotFound.Add(1)
+ }
+ p.readers[i] = nil
+ // This will be communicated upstream.
+ p.orgReaders[bufIdx] = nil
+ closeReader = true
+ }
+ p.stateLK.Unlock()
+ if closeReader {
+ if closer, ok := rr.(io.Closer); ok {
+ closer.Close()
+ }
+ }
+ p.events <- hedgedResult{blk: blk, i: i, buf: buf, n: n, err: err}
+ }(i, bufIdx, rr, buf, offset)
+ return true
+ }
+ return false
+ }
+
+ for n := 0; n < p.dataBlocks+1; n++ {
+ // Launch dataBlocks+1 reads so that a single slow or
+ // missing shard is covered without waiting for the fan-out.
+ if !launchNext() {
+ break
+ }
+ }
+
+ fanoutPossible := false
+ for i := range p.readers {
+ if p.readers[i] != nil && !p.inflight[i] && !blk.launched[i] {
+ fanoutPossible = true
+ break
+ }
+ }
+ var timer *time.Timer
+ if fanoutPossible {
+ timer = time.AfterFunc(p.fanoutDelay, func() {
+ p.stateLK.Lock()
+ defer p.stateLK.Unlock()
+ if p.curBlock != blk {
+ return
+ }
+ for launchNext() {
+ }
+ })
+ }
+ p.stateLK.Unlock()
+
+ defer func() {
+ if timer != nil {
+ timer.Stop()
+ }
+ p.stateLK.Lock()
+ if p.curBlock == blk {
+ p.curBlock = nil
+ }
+ p.stateLK.Unlock()
+ }()
+
+ bufCount := 0
+ for bufCount < p.dataBlocks {
+ p.stateLK.Lock()
+ if blk.outstanding == 0 {
+ launched := launchNext()
+ waiting := false
+ if !launched {
+ for i := range p.inflight {
+ if p.inflight[i] {
+ waiting = true
+ break
+ }
+ }
+ }
+ p.stateLK.Unlock()
+ switch {
+ case launched:
+ continue
+ case waiting:
+ // All launchable readers are exhausted but stragglers
+ // of earlier blocks may still deliver.
+ default:
+ // If we cannot decode, just return read quorum error.
+ return nil, fmt.Errorf("%w (offline-disks=%d/%d)", errErasureReadQuorum, p.disksNotFound.Load(), len(p.readers))
+ }
+ } else {
+ p.stateLK.Unlock()
+ }
+
+ ev := <-p.events
+ p.stateLK.Lock()
+ if ev.blk == blk {
+ blk.outstanding--
+ }
+ switch {
+ case ev.blk == blk && ev.err == nil:
+ p.stateLK.Unlock()
+ newBuf[p.readerToBuf[ev.i]] = ev.buf[:ev.n]
+ bufCount++
+ case ev.blk == blk:
+ // Since ReadAt returned error, launch a substitute shard.
+ launchNext()
+ p.stateLK.Unlock()
+ default:
+ // Stale result of an earlier block; its reader has been
+ // marked available and can be relaunched on the next
+ // iteration.
+ p.stateLK.Unlock()
+ }
+ }
+
+ p.stateLK.Lock()
+ if p.curBlock == blk {
+ p.curBlock = nil
+ }
+ p.stateLK.Unlock()
+
+ p.offset += p.shardSize
+ switch {
+ case p.missingPartsHeal.Load():
+ return newBuf, errFileNotFound
+ case p.bitrotHeal.Load():
+ return newBuf, errFileCorrupt
+ }
+ return newBuf, nil
+}
+
// Decode reads from readers, reconstructs data if needed and writes the data to the writer.
// A set of preferred drives can be supplied. In that case they will be used and the data reconstructed.
func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []io.ReaderAt, offset, length, totalLength int64, prefer []bool) (written int64, derr error) {
@@ -248,7 +483,7 @@ func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []io.Read
return 0, nil
}
- reader := newParallelReader(readers, e, offset, totalLength)
+ reader := newParallelReader(readers, e, offset, totalLength, globalDriveConfig.GetReadFanoutDelay())
if len(prefer) == len(readers) {
reader.preferReaders(prefer)
}
@@ -319,7 +554,7 @@ func (e Erasure) Heal(ctx context.Context, writers []io.Writer, readers []io.Rea
return errInvalidArgument
}
- reader := newParallelReader(readers, e, 0, totalLength)
+ reader := newParallelReader(readers, e, 0, totalLength, 0)
if len(readers) == len(prefer) {
reader.preferReaders(prefer)
}
diff --git a/cmd/erasure-decode_test.go b/cmd/erasure-decode_test.go
index 229047e16a750..5d3bd6ca38c7a 100644
--- a/cmd/erasure-decode_test.go
+++ b/cmd/erasure-decode_test.go
@@ -21,11 +21,17 @@ import (
"bytes"
"context"
crand "crypto/rand"
+ "errors"
"io"
"math/rand"
+ "sync"
+ "sync/atomic"
"testing"
+ "time"
"github.com/dustin/go-humanize"
+ "github.com/minio/minio/internal/bpool"
+ "github.com/minio/minio/internal/config/drive"
)
func (a badDisk) ReadFile(ctx context.Context, volume string, path string, offset int64, buf []byte, verifier *BitrotVerifier) (n int64, err error) {
@@ -84,6 +90,12 @@ var erasureDecodeTests = []struct {
}
func TestErasureDecode(t *testing.T) {
+ // The streaming bitrot writer draws from the global byte pool
+ // which is otherwise only initialized during server pool setup.
+ if globalBytePoolCap.Load() == nil {
+ globalBytePoolCap.Store(bpool.NewBytePoolCap(64, int(blockSizeV2), 2*int(blockSizeV2)))
+ }
+
for i, test := range erasureDecodeTests {
setup, err := newErasureTestSetup(t, test.dataBlocks, test.onDisks-test.dataBlocks, test.blocksize)
if err != nil {
@@ -201,6 +213,11 @@ func TestErasureDecodeRandomOffsetLength(t *testing.T) {
if testing.Short() {
t.Skip()
}
+ // The streaming bitrot writer draws from the global byte pool
+ // which is otherwise only initialized during server pool setup.
+ if globalBytePoolCap.Load() == nil {
+ globalBytePoolCap.Store(bpool.NewBytePoolCap(64, int(blockSizeV2), 2*int(blockSizeV2)))
+ }
// Initialize environment needed for the test.
dataBlocks := 7
parityBlocks := 7
@@ -383,3 +400,406 @@ func BenchmarkErasureDecode_16_40MB(b *testing.B) {
b.Run(" XXXX0000|XXXX0000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 4, 4, size, b) })
b.Run(" XXXXXXXX|00000000 ", func(b *testing.B) { benchmarkErasureDecode(8, 8, 8, 0, size, b) })
}
+
+// gatedReaderAt blocks the first ReadAt call until gate is closed.
+// All later calls pass through. started is closed once the first
+// ReadAt is blocked, done once it has returned.
+type gatedReaderAt struct {
+ inner io.ReaderAt
+ gate chan struct{}
+ started chan struct{}
+ done chan struct{}
+ block sync.Once
+ calls atomic.Int32
+}
+
+func (g *gatedReaderAt) ReadAt(p []byte, off int64) (int, error) {
+ g.calls.Add(1)
+ g.block.Do(func() {
+ close(g.started)
+ <-g.gate
+ close(g.done)
+ })
+ return g.inner.ReadAt(p, off)
+}
+
+func (g *gatedReaderAt) Close() error {
+ if c, ok := g.inner.(io.Closer); ok {
+ return c.Close()
+ }
+ return nil
+}
+
+// missingShardReader always fails with errFileNotFound.
+type missingShardReader struct{}
+
+func (missingShardReader) ReadAt([]byte, int64) (int, error) { return 0, errFileNotFound }
+
+// hedgedEncodeShards encodes data block by block and returns the
+// shard files, mimicking the layout produced by Erasure.Encode.
+func hedgedEncodeShards(t *testing.T, e Erasure, data []byte) [][]byte {
+ t.Helper()
+ shards := make([][]byte, e.dataBlocks+e.parityBlocks)
+ for off := 0; off < len(data); off += int(e.blockSize) {
+ chunk := data[off:]
+ if len(chunk) > int(e.blockSize) {
+ chunk = chunk[:int(e.blockSize)]
+ }
+ encoded, err := e.EncodeData(t.Context(), chunk)
+ if err != nil {
+ t.Fatalf("EncodeData failed: %v", err)
+ }
+ for i := range shards {
+ shards[i] = append(shards[i], encoded[i]...)
+ }
+ }
+ return shards
+}
+
+func hedgedDecodeShards(t *testing.T, e Erasure, bufs [][]byte) []byte {
+ t.Helper()
+ if err := e.DecodeDataBlocks(bufs); err != nil {
+ t.Fatalf("DecodeDataBlocks failed: %v", err)
+ }
+ var out []byte
+ for i := 0; i < e.dataBlocks; i++ {
+ out = append(out, bufs[i]...)
+ }
+ return out
+}
+
+func hedgedDeliveredCount(bufs [][]byte) int {
+ n := 0
+ for _, b := range bufs {
+ if len(b) > 0 {
+ n++
+ }
+ }
+ return n
+}
+
+func hedgedShardReaders(t *testing.T, shards [][]byte) []io.ReaderAt {
+ t.Helper()
+ readers := make([]io.ReaderAt, len(shards))
+ for i := range shards {
+ readers[i] = bytes.NewReader(shards[i])
+ }
+ return readers
+}
+
+// TestParallelReaderHedgedOneSlow verifies that a single slow shard is
+// covered by the dataBlocks+1 initial launch without waiting for the
+// fan-out delay or the slow disk.
+func TestParallelReaderHedgedOneSlow(t *testing.T) {
+ const dataBlocks = 3
+ e, err := NewErasure(t.Context(), dataBlocks, 2, 30)
+ if err != nil {
+ t.Fatal(err)
+ }
+ data := make([]byte, 60)
+ for i := range data {
+ data[i] = byte(i)
+ }
+ shards := hedgedEncodeShards(t, e, data)
+ readers := hedgedShardReaders(t, shards)
+
+ gated := &gatedReaderAt{
+ inner: readers[0],
+ gate: make(chan struct{}),
+ started: make(chan struct{}),
+ done: make(chan struct{}),
+ }
+ readers[0] = gated
+
+ const fanoutDelay = 10 * time.Second
+ p := newParallelReader(readers, e, 0, int64(len(data)), fanoutDelay)
+ defer p.Done()
+
+ for block := 0; block < 2; block++ {
+ start := time.Now()
+ bufs, err := p.Read(nil)
+ if err != nil {
+ t.Fatalf("block %d: Read failed: %v", block, err)
+ }
+ if elapsed := time.Since(start); elapsed > 5*time.Second {
+ t.Fatalf("block %d: Read blocked for %v, hedged read should not wait for the slow shard", block, elapsed)
+ }
+ if got := hedgedDeliveredCount(bufs); got != dataBlocks {
+ t.Fatalf("block %d: got %d delivered shards, want %d", block, got, dataBlocks)
+ }
+ if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data[block*30:(block+1)*30]) {
+ t.Fatalf("block %d: decoded data mismatch", block)
+ }
+ }
+
+ // The slow reader was launched once per block, the never-needed
+ // 5th shard was never launched since the hedge sufficed.
+ if got := gated.calls.Load(); got != 1 {
+ t.Fatalf("gated reader launched %d times, want 1", got)
+ }
+
+ p.Done()
+ p.stateLK.Lock()
+ if readers[0] != nil {
+ t.Fatal("straggler reader should be marked nil in orgReaders after Done()")
+ }
+ p.stateLK.Unlock()
+
+ close(gated.gate)
+ <-gated.done
+}
+
+// TestParallelReaderHedgedFanout verifies that the fan-out timer
+// launches the remaining shards after fanoutDelay when two shards
+// are slow.
+func TestParallelReaderHedgedFanout(t *testing.T) {
+ const dataBlocks = 3
+ e, err := NewErasure(t.Context(), dataBlocks, 2, 30)
+ if err != nil {
+ t.Fatal(err)
+ }
+ data := make([]byte, 30)
+ for i := range data {
+ data[i] = byte(i)
+ }
+ shards := hedgedEncodeShards(t, e, data)
+ readers := hedgedShardReaders(t, shards)
+
+ gated0 := &gatedReaderAt{inner: readers[0], gate: make(chan struct{}), started: make(chan struct{}), done: make(chan struct{})}
+ gated1 := &gatedReaderAt{inner: readers[1], gate: make(chan struct{}), started: make(chan struct{}), done: make(chan struct{})}
+ readers[0] = gated0
+ readers[1] = gated1
+
+ const fanoutDelay = 300 * time.Millisecond
+ p := newParallelReader(readers, e, 0, int64(len(data)), fanoutDelay)
+ defer p.Done()
+
+ start := time.Now()
+ bufs, err := p.Read(nil)
+ if err != nil {
+ t.Fatalf("Read failed: %v", err)
+ }
+ if elapsed := time.Since(start); elapsed < 200*time.Millisecond {
+ t.Fatalf("Read returned after %v, fan-out delay did not trigger", elapsed)
+ }
+ if elapsed := time.Since(start); elapsed > 5*time.Second {
+ t.Fatalf("Read returned after %v, too slow", elapsed)
+ }
+ if got := hedgedDeliveredCount(bufs); got != dataBlocks {
+ t.Fatalf("got %d delivered shards, want %d", got, dataBlocks)
+ }
+ if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data) {
+ t.Fatal("decoded data mismatch")
+ }
+
+ close(gated0.gate)
+ close(gated1.gate)
+}
+
+// TestParallelReaderHedgedErrorSubstitution verifies that a shard
+// failing immediately is substituted right away, without waiting
+// for the fan-out delay.
+func TestParallelReaderHedgedErrorSubstitution(t *testing.T) {
+ const dataBlocks = 3
+ e, err := NewErasure(t.Context(), dataBlocks, 2, 30)
+ if err != nil {
+ t.Fatal(err)
+ }
+ data := make([]byte, 30)
+ for i := range data {
+ data[i] = byte(i)
+ }
+ shards := hedgedEncodeShards(t, e, data)
+ readers := hedgedShardReaders(t, shards)
+ readers[0] = missingShardReader{}
+
+ const fanoutDelay = 10 * time.Second
+ p := newParallelReader(readers, e, 0, int64(len(data)), fanoutDelay)
+ defer p.Done()
+
+ start := time.Now()
+ bufs, err := p.Read(nil)
+ if !errors.Is(err, errFileNotFound) {
+ t.Fatalf("expected errFileNotFound heal flag, got %v", err)
+ }
+ if elapsed := time.Since(start); elapsed > 5*time.Second {
+ t.Fatalf("Read blocked for %v, substitution should be immediate", elapsed)
+ }
+ if got := hedgedDeliveredCount(bufs); got != dataBlocks {
+ t.Fatalf("got %d delivered shards, want %d", got, dataBlocks)
+ }
+ if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data) {
+ t.Fatal("decoded data mismatch")
+ }
+}
+
+// TestParallelReaderHedgedQuorum verifies the read quorum error when
+// too many shards fail.
+func TestParallelReaderHedgedQuorum(t *testing.T) {
+ const dataBlocks = 3
+ e, err := NewErasure(t.Context(), dataBlocks, 2, 30)
+ if err != nil {
+ t.Fatal(err)
+ }
+ data := make([]byte, 30)
+ shards := hedgedEncodeShards(t, e, data)
+ readers := hedgedShardReaders(t, shards)
+ readers[0] = missingShardReader{}
+ readers[1] = missingShardReader{}
+ readers[2] = missingShardReader{}
+
+ p := newParallelReader(readers, e, 0, int64(len(data)), 10*time.Second)
+ defer p.Done()
+
+ bufs, err := p.Read(nil)
+ if !errors.Is(err, errErasureReadQuorum) {
+ t.Fatalf("expected errErasureReadQuorum, got %v", err)
+ }
+ if bufs != nil {
+ t.Fatal("expected nil bufs on quorum error")
+ }
+}
+
+// TestParallelReaderHedgedRejoin verifies that a straggler that
+// completes after its block returned rejoins for the next block.
+func TestParallelReaderHedgedRejoin(t *testing.T) {
+ const dataBlocks = 3
+ e, err := NewErasure(t.Context(), dataBlocks, 2, 30)
+ if err != nil {
+ t.Fatal(err)
+ }
+ data := make([]byte, 60)
+ for i := range data {
+ data[i] = byte(i)
+ }
+ shards := hedgedEncodeShards(t, e, data)
+ readers := hedgedShardReaders(t, shards)
+
+ gated := &gatedReaderAt{
+ inner: readers[0],
+ gate: make(chan struct{}),
+ started: make(chan struct{}),
+ done: make(chan struct{}),
+ }
+ readers[0] = gated
+
+ p := newParallelReader(readers, e, 0, int64(len(data)), 10*time.Second)
+ defer p.Done()
+
+ // Block 0: reader 0 straggles, delivered via hedge.
+ bufs, err := p.Read(nil)
+ if err != nil {
+ t.Fatalf("block 0: Read failed: %v", err)
+ }
+ if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data[0:30]) {
+ t.Fatal("block 0: decoded data mismatch")
+ }
+
+ // Release the straggler and wait until it is available again.
+ close(gated.gate)
+ deadline := time.Now().Add(5 * time.Second)
+ for {
+ p.stateLK.Lock()
+ inflight := p.inflight[0]
+ p.stateLK.Unlock()
+ if !inflight {
+ break
+ }
+ if time.Now().After(deadline) {
+ t.Fatal("straggler never completed")
+ }
+ time.Sleep(time.Millisecond)
+ }
+
+ // Block 1: reader 0 must have rejoined and be launched again.
+ bufs, err = p.Read(nil)
+ if err != nil {
+ t.Fatalf("block 1: Read failed: %v", err)
+ }
+ if got := hedgedDeliveredCount(bufs); got != dataBlocks {
+ t.Fatalf("block 1: got %d delivered shards, want %d", got, dataBlocks)
+ }
+ if got := hedgedDecodeShards(t, e, bufs); !bytes.Equal(got, data[30:60]) {
+ t.Fatal("block 1: decoded data mismatch")
+ }
+ if got := gated.calls.Load(); got != 2 {
+ t.Fatalf("gated reader launched %d times, want 2", got)
+ }
+}
+
+// TestErasureDecodeHedgedSlowDisk verifies the full decode path with
+// hedged reads enabled: a disk stuck in a read does not stall the
+// object read.
+func TestErasureDecodeHedgedSlowDisk(t *testing.T) {
+ // The streaming bitrot writer draws from the global byte pool
+ // which is otherwise only initialized during server pool setup.
+ if globalBytePoolCap.Load() == nil {
+ globalBytePoolCap.Store(bpool.NewBytePoolCap(64, int(blockSizeV2), 2*int(blockSizeV2)))
+ }
+
+ dataBlocks := 3
+ parityBlocks := 2
+ setup, err := newErasureTestSetup(t, dataBlocks, parityBlocks, blockSizeV2)
+ if err != nil {
+ t.Fatalf("failed to create test setup: %v", err)
+ }
+ erasure, err := NewErasure(t.Context(), dataBlocks, parityBlocks, blockSizeV2)
+ if err != nil {
+ t.Fatalf("failed to create ErasureStorage: %v", err)
+ }
+
+ data := make([]byte, oneMiByte)
+ if _, err = io.ReadFull(crand.Reader, data); err != nil {
+ t.Fatal(err)
+ }
+
+ buffer := make([]byte, blockSizeV2, 2*blockSizeV2)
+ writers := make([]io.Writer, len(setup.disks))
+ for i, disk := range setup.disks {
+ writers[i] = newBitrotWriter(disk, "", "testbucket", "object", erasure.ShardFileSize(int64(len(data))), DefaultBitrotAlgorithm, erasure.ShardSize())
+ }
+ if _, err = erasure.Encode(t.Context(), bytes.NewReader(data), writers, buffer, erasure.dataBlocks+1); err != nil {
+ t.Fatal(err)
+ }
+ closeBitrotWriters(writers)
+
+ bitrotReaders := make([]io.ReaderAt, len(setup.disks))
+ for index, disk := range setup.disks {
+ tillOffset := erasure.ShardFileOffset(0, int64(len(data)), int64(len(data)))
+ bitrotReaders[index] = newBitrotReader(disk, nil, "testbucket", "object", tillOffset, DefaultBitrotAlgorithm, bitrotWriterSum(writers[index]), erasure.ShardSize())
+ }
+ gated := &gatedReaderAt{
+ inner: bitrotReaders[0],
+ gate: make(chan struct{}),
+ started: make(chan struct{}),
+ done: make(chan struct{}),
+ }
+ bitrotReaders[0] = gated
+
+ globalDriveConfig.Update(drive.Config{ReadFanoutDelay: 10 * time.Second})
+ defer globalDriveConfig.Update(drive.Config{})
+
+ writer := bytes.NewBuffer(nil)
+ start := time.Now()
+ _, err = erasure.Decode(t.Context(), writer, bitrotReaders, 0, int64(len(data)), int64(len(data)), nil)
+ if err != nil {
+ t.Fatalf("Decode failed: %v", err)
+ }
+ if elapsed := time.Since(start); elapsed > 5*time.Second {
+ t.Fatalf("Decode blocked for %v, slow disk should not stall the read", elapsed)
+ }
+ if !bytes.Equal(writer.Bytes(), data) {
+ t.Fatal("read returns wrong file content")
+ }
+
+ // The straggler is marked in the caller's reader slice so that
+ // closeBitrotReaders skips it; its goroutine closes it after the
+ // read returns.
+ if bitrotReaders[0] != nil {
+ t.Fatal("straggler reader should be marked nil for the caller")
+ }
+
+ close(gated.gate)
+ <-gated.done
+ closeBitrotReaders(bitrotReaders)
+}
diff --git a/cmd/handler-utils.go b/cmd/handler-utils.go
index 7752cfece2db3..e8ef441f8774d 100644
--- a/cmd/handler-utils.go
+++ b/cmd/handler-utils.go
@@ -29,6 +29,7 @@ import (
"github.com/minio/madmin-go/v3"
"github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/bucket/replication"
"github.com/minio/minio/internal/handlers"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
@@ -142,15 +143,18 @@ var userMetadataKeyPrefixes = []string{
// extractMetadataFromReq extracts metadata from HTTP header and HTTP queryString.
func extractMetadataFromReq(ctx context.Context, r *http.Request) (metadata map[string]string, err error) {
- return extractMetadata(ctx, textproto.MIMEHeader(r.Form), textproto.MIMEHeader(r.Header))
+ // CVE-2026-34204: only replication traffic is allowed to set internal SSE
+ // metadata via X-Minio-Replication-* headers.
+ isReplica := r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String()
+ return extractMetadata(ctx, isReplica, textproto.MIMEHeader(r.Form), textproto.MIMEHeader(r.Header))
}
-func extractMetadata(ctx context.Context, mimesHeader ...textproto.MIMEHeader) (metadata map[string]string, err error) {
+func extractMetadata(ctx context.Context, isReplica bool, mimesHeader ...textproto.MIMEHeader) (metadata map[string]string, err error) {
metadata = make(map[string]string)
for _, hdr := range mimesHeader {
// Extract all query values.
- err = extractMetadataFromMime(ctx, hdr, metadata)
+ err = extractMetadataFromMime(ctx, hdr, metadata, isReplica)
if err != nil {
return nil, err
}
@@ -191,7 +195,9 @@ func extractMetadata(ctx context.Context, mimesHeader ...textproto.MIMEHeader) (
}
// extractMetadata extracts metadata from map values.
-func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[string]string) error {
+// When allowReplicationHeaders is false, X-Minio-Replication-* headers that
+// map to internal SSE metadata are ignored (CVE-2026-34204).
+func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[string]string, allowReplicationHeaders bool) error {
if v == nil {
bugLogIf(ctx, errInvalidArgument)
return errInvalidArgument
@@ -208,6 +214,9 @@ func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[
value, ok := nv[http.CanonicalHeaderKey(supportedHeader)]
if ok {
if v, ok := replicationToInternalHeaders[supportedHeader]; ok {
+ if !allowReplicationHeaders {
+ continue
+ }
m[v] = strings.Join(value, ",")
} else {
m[supportedHeader] = strings.Join(value, ",")
diff --git a/cmd/handler-utils_test.go b/cmd/handler-utils_test.go
index 517f93fcc9fbd..caaec920a9e26 100644
--- a/cmd/handler-utils_test.go
+++ b/cmd/handler-utils_test.go
@@ -158,12 +158,21 @@ func TestExtractMetadataHeaders(t *testing.T) {
metadata: nil,
shouldFail: true,
},
+ // CVE-2026-34204: X-Minio-Replication-* SSE headers must not be
+ // accepted from regular (non-replication) requests.
+ {
+ header: http.Header{
+ "X-Minio-Replication-Server-Side-Encryption-Sealed-Key": []string{"injected-value"},
+ },
+ metadata: map[string]string{},
+ shouldFail: false,
+ },
}
// Validate if the extracting headers.
for i, testCase := range testCases {
metadata := make(map[string]string)
- err := extractMetadataFromMime(t.Context(), textproto.MIMEHeader(testCase.header), metadata)
+ err := extractMetadataFromMime(t.Context(), textproto.MIMEHeader(testCase.header), metadata, false)
if err != nil && !testCase.shouldFail {
t.Fatalf("Test %d failed to extract metadata: %v", i+1, err)
}
@@ -176,6 +185,24 @@ func TestExtractMetadataHeaders(t *testing.T) {
}
}
+// Tests that replication SSE headers are accepted when explicitly allowed.
+func TestExtractMetadataHeadersReplicationAllowed(t *testing.T) {
+ metadata := make(map[string]string)
+ header := http.Header{
+ "X-Minio-Replication-Server-Side-Encryption-Sealed-Key": []string{"replica-value"},
+ }
+ err := extractMetadataFromMime(t.Context(), textproto.MIMEHeader(header), metadata, true)
+ if err != nil {
+ t.Fatalf("failed to extract metadata: %v", err)
+ }
+ expected := map[string]string{
+ "X-Minio-Internal-Server-Side-Encryption-Sealed-Key": "replica-value",
+ }
+ if !reflect.DeepEqual(metadata, expected) {
+ t.Fatalf("Expected %#v, got %#v", expected, metadata)
+ }
+}
+
// Test getResource()
func TestGetResource(t *testing.T) {
testCases := []struct {
diff --git a/cmd/object-handlers.go b/cmd/object-handlers.go
index 6542c46554f89..dfc77b3db2935 100644
--- a/cmd/object-handlers.go
+++ b/cmd/object-handlers.go
@@ -2242,7 +2242,7 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
// if Content-Length is unknown/missing, deny the request
size := r.ContentLength
rAuthType := getRequestAuthType(r)
- if rAuthType == authTypeStreamingSigned || rAuthType == authTypeStreamingSignedTrailer {
+ if rAuthType == authTypeStreamingSigned || rAuthType == authTypeStreamingSignedTrailer || rAuthType == authTypeStreamingUnsignedTrailer {
if sizeStr, ok := r.Header[xhttp.AmzDecodedContentLength]; ok {
if sizeStr[0] == "" {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
@@ -2296,6 +2296,13 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
}
+ case authTypeStreamingUnsignedTrailer:
+ // Initialize stream chunked reader with optional trailers.
+ reader, s3Err = newUnsignedV4ChunkedReader(r, true, r.Header.Get(xhttp.Authorization) != "")
+ if s3Err != ErrNone {
+ writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
+ return
+ }
case authTypeSignedV2, authTypePresignedV2:
s3Err = isReqAuthenticatedV2(r)
if s3Err != ErrNone {
@@ -2413,7 +2420,8 @@ func (api objectAPIHandlers) PutObjectExtractHandler(w http.ResponseWriter, r *h
hdrs.Set(k, v)
}
}
- m, err := extractMetadata(ctx, textproto.MIMEHeader(hdrs))
+ isReplica := r.Header.Get(xhttp.AmzBucketReplicationStatus) == replication.Replica.String()
+ m, err := extractMetadata(ctx, isReplica, textproto.MIMEHeader(hdrs))
if err != nil {
return err
}
diff --git a/cmd/server_test.go b/cmd/server_test.go
index e69117351544b..97a8650b04e41 100644
--- a/cmd/server_test.go
+++ b/cmd/server_test.go
@@ -18,6 +18,7 @@
package cmd
import (
+ "archive/tar"
"bytes"
"context"
"encoding/xml"
@@ -128,6 +129,8 @@ func runAllTests(suite *TestSuiteCommon, c *check) {
suite.TestBucketSQSNotificationWebHook(c)
suite.TestBucketSQSNotificationAMQP(c)
suite.TestUnsignedCVE(c)
+ suite.TestUnsignedTrailerQueryStringCVE(c)
+ suite.TestSnowballAutoExtractCVE(c)
suite.TearDownSuite(c)
}
@@ -409,6 +412,117 @@ func (s *TestSuiteCommon) TestUnsignedCVE(c *check) {
c.Assert(response.StatusCode, http.StatusBadRequest)
}
+func (s *TestSuiteCommon) TestUnsignedTrailerQueryStringCVE(c *check) {
+ c.Helper()
+
+ // generate a random bucket Name.
+ bucketName := getRandomBucketName()
+
+ // HTTP request to create the bucket.
+ request, err := newTestSignedRequest(http.MethodPut, getMakeBucketURL(s.endPoint, bucketName),
+ 0, nil, s.accessKey, s.secretKey, s.signer)
+ c.Assert(err, nil)
+
+ // execute the request.
+ response, err := s.client.Do(request)
+ c.Assert(err, nil)
+ c.Assert(response.StatusCode, http.StatusOK)
+
+ now := UTCNow()
+ scope := fmt.Sprintf("%s/us-east-1/s3/aws4_request", now.Format(yyyymmdd))
+ putURL := getPutObjectURL(s.endPoint, bucketName, "test-cve-object.txt")
+ // Inject a valid access key via query string, mimicking the
+ // CVE-2026-41145 attack vector.
+ putURL = putURL + "?" + xhttp.AmzCredential + "=" + url.QueryEscape(s.accessKey+"/"+scope)
+
+ req, err := http.NewRequest(http.MethodPut, putURL, nil)
+ c.Assert(err, nil)
+
+ req.Body = io.NopCloser(bytes.NewReader([]byte("foobar!\n")))
+ req.Trailer = http.Header{}
+ req.Trailer.Set("x-amz-checksum-crc32", "rK0DXg==")
+
+ req = signer.StreamingUnsignedV4(req, "", 8, now)
+
+ // Remove the Authorization header; the attacker only provides the
+ // access key via the query string and does not know the secret key.
+ req.Header.Del("Authorization")
+
+ // Ensure the headers expected for unsigned trailer streaming remain.
+ req.Header.Set("X-Amz-Decoded-Content-Length", "8")
+ req.Header.Set("Content-Encoding", "aws-chunked")
+ req.Header.Set("X-Amz-Trailer", "x-amz-checksum-crc32")
+ req.Header.Set("x-amz-content-sha256", unsignedPayloadTrailer)
+
+ // execute the request.
+ response, err = s.client.Do(req)
+ c.Assert(err, nil)
+
+ // The request must be rejected because query-string credentials are not
+ // signature-verified for unsigned trailer streaming.
+ c.Assert(response.StatusCode, http.StatusForbidden)
+}
+
+func (s *TestSuiteCommon) TestSnowballAutoExtractCVE(c *check) {
+ c.Helper()
+
+ // generate a random bucket Name.
+ bucketName := getRandomBucketName()
+
+ // HTTP request to create the bucket.
+ request, err := newTestSignedRequest(http.MethodPut, getMakeBucketURL(s.endPoint, bucketName),
+ 0, nil, s.accessKey, s.secretKey, s.signer)
+ c.Assert(err, nil)
+
+ // execute the request.
+ response, err := s.client.Do(request)
+ c.Assert(err, nil)
+ c.Assert(response.StatusCode, http.StatusOK)
+
+ // Build a minimal valid tar archive.
+ var tarBuf bytes.Buffer
+ tw := tar.NewWriter(&tarBuf)
+ c.Assert(tw.WriteHeader(&tar.Header{
+ Name: "test.txt",
+ Mode: 0644,
+ Size: int64(len("hello snowball")),
+ }), nil)
+ _, err = tw.Write([]byte("hello snowball"))
+ c.Assert(err, nil)
+ c.Assert(tw.Close(), nil)
+
+ now := UTCNow()
+ req, err := http.NewRequest(http.MethodPut, getPutObjectURL(s.endPoint, bucketName, "test-snowball.tar"), nil)
+ c.Assert(err, nil)
+
+ req.Body = io.NopCloser(bytes.NewReader(tarBuf.Bytes()))
+ req.Trailer = http.Header{}
+ req.Trailer.Set("x-amz-checksum-crc32", "rK0DXg==")
+
+ req = signer.StreamingUnsignedV4(req, "", int64(tarBuf.Len()), now)
+
+ // The CVE-2026-40344 attack: provide an Authorization header with a
+ // valid access key but a completely fabricated signature. The Snowball
+ // auto-extract handler must still verify the signature.
+ maliciousAuth := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=deadbeefdeadbeefdeadbeeddeadbeeddeadbeefdeadbeefdeadbeefdeadbeef", s.accessKey, now.Format(yyyymmdd))
+ req.Header.Set("Authorization", maliciousAuth)
+
+ // Ensure the headers expected for unsigned trailer streaming remain.
+ req.Header.Set("X-Amz-Decoded-Content-Length", fmt.Sprintf("%d", tarBuf.Len()))
+ req.Header.Set("Content-Encoding", "aws-chunked")
+ req.Header.Set("X-Amz-Trailer", "x-amz-checksum-crc32")
+ req.Header.Set("x-amz-content-sha256", unsignedPayloadTrailer)
+ // Trigger the Snowball auto-extract handler.
+ req.Header.Set("X-Amz-Meta-Snowball-Auto-Extract", "true")
+
+ // execute the request.
+ response, err = s.client.Do(req)
+ c.Assert(err, nil)
+
+ // The request must be rejected because the fabricated signature is not valid.
+ c.Assert(response.StatusCode, http.StatusForbidden)
+}
+
func (s *TestSuiteCommon) TestBucketSQSNotificationAMQP(c *check) {
// Sample bucket notification.
bucketNotificationBuf := `s3:ObjectCreated:Putprefiximages/1arn:minio:sqs:us-east-1:444455556666:amqp`
diff --git a/cmd/signature-v4-parser.go b/cmd/signature-v4-parser.go
index f6866cb858870..9d45590586921 100644
--- a/cmd/signature-v4-parser.go
+++ b/cmd/signature-v4-parser.go
@@ -66,6 +66,23 @@ func getReqAccessKeyV4(r *http.Request, region string, stype serviceType) (auth.
return checkKeyValid(r, ch.accessKey)
}
+// getReqAccessKeyV4FromHeader extracts the access key only from the
+// Authorization header. It is used for auth types (such as unsigned trailer
+// streaming) where query-string credentials must not be trusted because no
+// signature verification is performed for them.
+func getReqAccessKeyV4FromHeader(r *http.Request, region string, stype serviceType) (auth.Credentials, bool, APIErrorCode) {
+ v4Auth := strings.TrimPrefix(r.Header.Get("Authorization"), signV4Algorithm)
+ authFields := strings.Split(strings.TrimSpace(v4Auth), ",")
+ if len(authFields) != 3 {
+ return auth.Credentials{}, false, ErrMissingFields
+ }
+ ch, s3Err := parseCredentialHeader(authFields[0], region, stype)
+ if s3Err != ErrNone {
+ return auth.Credentials{}, false, s3Err
+ }
+ return checkKeyValid(r, ch.accessKey)
+}
+
// parse credentialHeader string into its structured form.
func parseCredentialHeader(credElement string, region string, stype serviceType) (ch credentialHeader, aec APIErrorCode) {
creds := strings.SplitN(strings.TrimSpace(credElement), "=", 2)
diff --git a/docs/changelog/hedged-erasure-reads.md b/docs/changelog/hedged-erasure-reads.md
new file mode 100644
index 0000000000000..b9e3a04c8b8da
--- /dev/null
+++ b/docs/changelog/hedged-erasure-reads.md
@@ -0,0 +1,77 @@
+# Changelog: hedged erasure reads (read_fanout_delay)
+
+## Summary
+
+Adds an opt-in "hedged read" strategy for erasure-coded object reads
+(`GET`/range reads). A single slow drive no longer stalls a read; the
+read races `K+1` shards (K = data blocks) and, if that is not enough,
+fans out to all remaining shards after a configurable delay.
+
+## Configuration
+
+New `drive` subsystem setting, applied dynamically (no restart):
+
+```
+mc admin config set drive.read_fanout_delay=250ms
+```
+
+- `0` (default): current behavior, hedging disabled.
+- `250ms` (example): enable hedged reads; value is per-block fan-out delay.
+- Environment override: `MINIO_DRIVE_READ_FANOUT_DELAY=250ms`.
+ The environment variable takes precedence over the config value.
+
+## Behavior when enabled
+
+Per erasure block (`Erasure.Decode`, normal object reads only):
+
+1. Launches reads on the first `K+1` online shards (local disks preferred
+ via the existing `prefer` ordering). One slow or missing shard is
+ covered instantly at the cost of a single extra shard read.
+2. A shard that fails immediately (missing, corrupt, disk error) triggers
+ an immediate substitute read, as before.
+3. If `K` verified shards have not arrived within `read_fanout_delay`,
+ reads are launched on every remaining shard in parallel.
+4. The block completes as soon as `K` bitrot-verified shards (data or
+ parity) have arrived; the decode step reconstructs missing data
+ shards from whatever mix arrived.
+5. Readers that lose the race ("stragglers") are not waited on. A
+ straggler that completes late is discarded for its own block and
+ rejoins for the next block; while slow it is simply skipped.
+
+Healing (`Erasure.Heal`) and all write paths are unchanged. Read quorum
+failure still surfaces as `SlowDownRead` (503).
+
+## Overheads (when enabled)
+
+- Steady state: one extra shard read per block (`K+1` instead of `K`).
+- Full fan-out reads only on blocks that actually stall for the
+ configured delay; shards read after the block completed are wasted
+ reads, bounded by the fan-out.
+- RAM: shard buffers are allocated per launched reader instead of
+ seeded from the shared byte pool (straggler goroutines may outlive
+ the reader); worst case is `N x shardSize` per in-flight part read.
+
+## Operational notes
+
+- Sensible starting point: `250ms`. Must stay well below
+ `drive.max_timeout` (default 30s).
+- Bitrot verification applies to every shard that wins the race, so
+ the integrity guarantees are unchanged: corrupt shards are replaced
+ by parity on the fly and the object is queued for async heal.
+- Tune with the existing disk metrics (`total-errs-timeout`) and
+ observe straggler frequency before lowering `drive.max_timeout`; a
+ slow drive now loses block races instead of blocking them, so the
+ two settings compound.
+
+## Files changed
+
+- `internal/config/drive/drive.go`, `internal/config/drive/help.go`:
+ new `read_fanout_delay` key, env override, dynamic update, help text.
+- `cmd/erasure-decode.go`: hedged read implementation in
+ `parallelReader` (`readHedged`), in-flight reader tracking, straggler
+ re-join/abandon lifecycle; `Erasure.Decode` wires the config,
+ `Erasure.Heal` stays on the plain path.
+- `cmd/erasure-decode_test.go`: hedged unit tests (hedge coverage,
+ fan-out timer, error substitution, quorum error, straggler rejoin),
+ full-stack slow-disk decode test, and byte-pool self-initialization
+ for the decode tests (fixes pre-existing panic when run standalone).
\ No newline at end of file
diff --git a/internal/config/drive/drive.go b/internal/config/drive/drive.go
index 862c62ab76b79..7f4186754c8dd 100644
--- a/internal/config/drive/drive.go
+++ b/internal/config/drive/drive.go
@@ -18,6 +18,7 @@
package drive
import (
+ "fmt"
"sync"
"time"
@@ -30,6 +31,7 @@ const (
EnvMaxDriveTimeout = "MINIO_DRIVE_MAX_TIMEOUT"
EnvMaxDriveTimeoutLegacy = "_MINIO_DRIVE_MAX_TIMEOUT"
EnvMaxDiskTimeoutLegacy = "_MINIO_DISK_MAX_TIMEOUT"
+ EnvReadFanoutDelay = "MINIO_DRIVE_READ_FANOUT_DELAY"
)
// DefaultKVS - default KVS for drive
@@ -38,6 +40,10 @@ var DefaultKVS = config.KVS{
Key: MaxTimeout,
Value: "30s",
},
+ config.KV{
+ Key: ReadFanoutDelay,
+ Value: "0",
+ },
}
var configLk sync.RWMutex
@@ -46,6 +52,10 @@ var configLk sync.RWMutex
type Config struct {
// MaxTimeout - maximum timeout for a drive operation
MaxTimeout time.Duration `json:"maxTimeout"`
+
+ // ReadFanoutDelay - delay after which a hedged erasure read fans out
+ // to all remaining shards, 0 disables hedged reads.
+ ReadFanoutDelay time.Duration `json:"readFanoutDelay"`
}
// Update - updates the config with latest values
@@ -53,6 +63,7 @@ func (c *Config) Update(updated Config) error {
configLk.Lock()
defer configLk.Unlock()
c.MaxTimeout = getMaxTimeout(updated.MaxTimeout)
+ c.ReadFanoutDelay = updated.ReadFanoutDelay
return nil
}
@@ -61,6 +72,14 @@ func (c *Config) GetMaxTimeout() time.Duration {
return c.GetOPTimeout()
}
+// GetReadFanoutDelay - returns the delay after which hedged erasure
+// reads fan out to all remaining shards. 0 disables hedged reads.
+func (c *Config) GetReadFanoutDelay() time.Duration {
+ configLk.RLock()
+ defer configLk.RUnlock()
+ return c.ReadFanoutDelay
+}
+
// GetOPTimeout - returns the per call drive operation timeout
func (c *Config) GetOPTimeout() time.Duration {
configLk.RLock()
@@ -90,6 +109,16 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
cfg.MaxTimeout = getMaxTimeout(dur)
}
}
+
+ fanoutDelay, err := time.ParseDuration(env.Get(EnvReadFanoutDelay, kvs.GetWithDefault(ReadFanoutDelay, DefaultKVS)))
+ if err != nil {
+ return cfg, err
+ }
+ if fanoutDelay < 0 {
+ return cfg, fmt.Errorf("invalid value %v for read_fanout_delay", fanoutDelay)
+ }
+ cfg.ReadFanoutDelay = fanoutDelay
+
return cfg, err
}
diff --git a/internal/config/drive/help.go b/internal/config/drive/help.go
index 5964dcce4181f..37b42e313cb0b 100644
--- a/internal/config/drive/help.go
+++ b/internal/config/drive/help.go
@@ -23,6 +23,9 @@ var (
// MaxTimeout is the max timeout for drive
MaxTimeout = "max_timeout"
+ // ReadFanoutDelay is the delay before hedged erasure reads fan out to all shards
+ ReadFanoutDelay = "read_fanout_delay"
+
// HelpDrive is help for drive
HelpDrive = config.HelpKVS{
config.HelpKV{
@@ -31,5 +34,11 @@ var (
Description: "set per call max_timeout for the drive, defaults to 30 seconds",
Optional: true,
},
+ config.HelpKV{
+ Key: ReadFanoutDelay,
+ Type: "string",
+ Description: "hedged erasure read fan-out delay, e.g. '250ms'. Reads start with K+1 shards and fan out to all shards after this delay. '0' (default) disables hedged reads",
+ Optional: true,
+ },
}
)
diff --git a/internal/config/identity/openid/jwt.go b/internal/config/identity/openid/jwt.go
index 2a422010f842d..001c861235208 100644
--- a/internal/config/identity/openid/jwt.go
+++ b/internal/config/identity/openid/jwt.go
@@ -79,8 +79,14 @@ func (r *Config) PopulatePublicKey(arn arn.ARN) error {
return nil
}
- // Add client secret for the client ID for HMAC based signature.
- r.pubKeys.add(pCfg.ClientID, []byte(pCfg.ClientSecret))
+ // Add client secret for the client ID for HMAC based signature, but only
+ // when the provider advertises HMAC signing algorithms. Otherwise an
+ // attacker who knows the client secret could perform an algorithm
+ // confusion attack by sending a token with alg=HS256 even though the
+ // provider only issues RS256 tokens (CVE-2026-33322).
+ if hasHMACAlgorithm(pCfg.DiscoveryDoc.IDTokenSigningAlgValuesSupported) {
+ r.pubKeys.add(pCfg.ClientID, []byte(pCfg.ClientSecret))
+ }
client := &http.Client{
Transport: r.transport,
@@ -103,6 +109,18 @@ var (
ErrTokenExpired = errors.New("token expired")
)
+// hasHMACAlgorithm returns true if the provider advertises an HMAC-based
+// JWT signing algorithm.
+func hasHMACAlgorithm(algs []string) bool {
+ for _, alg := range algs {
+ switch alg {
+ case "HS256", "HS384", "HS512":
+ return true
+ }
+ }
+ return false
+}
+
func updateClaimsExpiry(dsecs string, claims map[string]any) error {
expStr := claims["exp"]
if expStr == "" {
@@ -134,13 +152,18 @@ const (
// Validate - validates the id_token.
func (r *Config) Validate(ctx context.Context, arn arn.ARN, token, accessToken, dsecs string, claims map[string]any) error {
+ pCfg, ok := r.arnProviderCfgsMap[arn]
+ if !ok {
+ return fmt.Errorf("Role %s does not exist", arn)
+ }
+
jp := new(jwtgo.Parser)
- jp.ValidMethods = []string{
- "RS256", "RS384", "RS512",
- "ES256", "ES384", "ES512",
- "HS256", "HS384", "HS512",
- "RS3256", "RS3384", "RS3512",
- "ES3256", "ES3384", "ES3512",
+ // Only accept the signing algorithms advertised by the provider. If the
+ // provider does not advertise any, fall back to asymmetric algorithms to
+ // prevent algorithm confusion attacks (CVE-2026-33322).
+ jp.ValidMethods = pCfg.DiscoveryDoc.IDTokenSigningAlgValuesSupported
+ if len(jp.ValidMethods) == 0 {
+ jp.ValidMethods = []string{"RS256", "RS384", "RS512", "ES256", "ES384", "ES512"}
}
keyFuncCallback := func(jwtToken *jwtgo.Token) (any, error) {
@@ -155,11 +178,6 @@ func (r *Config) Validate(ctx context.Context, arn arn.ARN, token, accessToken,
return pubkey, nil
}
- pCfg, ok := r.arnProviderCfgsMap[arn]
- if !ok {
- return fmt.Errorf("Role %s does not exist", arn)
- }
-
mclaims := jwtgo.MapClaims(claims)
jwtToken, err := jp.ParseWithClaims(token, &mclaims, keyFuncCallback)
if err != nil {
@@ -168,7 +186,7 @@ func (r *Config) Validate(ctx context.Context, arn arn.ARN, token, accessToken,
if err = r.PopulatePublicKey(arn); err != nil {
return err
}
- jwtToken, err = jwtgo.ParseWithClaims(token, &mclaims, keyFuncCallback)
+ jwtToken, err = jp.ParseWithClaims(token, &mclaims, keyFuncCallback)
if err != nil {
return err
}
diff --git a/internal/config/identity/openid/jwt_test.go b/internal/config/identity/openid/jwt_test.go
index cb54faff95741..7871614441bca 100644
--- a/internal/config/identity/openid/jwt_test.go
+++ b/internal/config/identity/openid/jwt_test.go
@@ -129,6 +129,9 @@ func TestJWTHMACType(t *testing.T) {
provider := providerCfg{
ClientID: "76b95ae5-33ef-4283-97b7-d2a85dc2d8f4",
ClientSecret: "WNGvKVyyNmXq0TraSvjaDN9CtpFgx35IXtGEffMCPR0",
+ DiscoveryDoc: DiscoveryDoc{
+ IDTokenSigningAlgValuesSupported: []string{"HS256"},
+ },
}
provider.JWKS.URL = u1
cfg := Config{
@@ -151,6 +154,71 @@ func TestJWTHMACType(t *testing.T) {
}
}
+// TestJWTAlgorithmConfusion verifies CVE-2026-33322: an attacker with the
+// client secret must not be able to forge an HS256 token when the provider
+// only advertises RS256.
+func TestJWTAlgorithmConfusion(t *testing.T) {
+ server := initJWKSServer()
+ defer server.Close()
+
+ jwt := &jwtgo.Token{
+ Method: jwtgo.SigningMethodHS256,
+ Claims: jwtgo.StandardClaims{
+ ExpiresAt: 253428928061,
+ Audience: "76b95ae5-33ef-4283-97b7-d2a85dc2d8f4",
+ },
+ Header: map[string]any{
+ "typ": "JWT",
+ "alg": jwtgo.SigningMethodHS256.Alg(),
+ "kid": "76b95ae5-33ef-4283-97b7-d2a85dc2d8f4",
+ },
+ }
+
+ token, err := jwt.SignedString([]byte("WNGvKVyyNmXq0TraSvjaDN9CtpFgx35IXtGEffMCPR0"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ u1, err := xnet.ParseHTTPURL(server.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ pubKeys := publicKeys{
+ RWMutex: &sync.RWMutex{},
+ pkMap: map[string]any{},
+ }
+ pubKeys.add("76b95ae5-33ef-4283-97b7-d2a85dc2d8f4", []byte("WNGvKVyyNmXq0TraSvjaDN9CtpFgx35IXtGEffMCPR0"))
+
+ provider := providerCfg{
+ ClientID: "76b95ae5-33ef-4283-97b7-d2a85dc2d8f4",
+ ClientSecret: "WNGvKVyyNmXq0TraSvjaDN9CtpFgx35IXtGEffMCPR0",
+ DiscoveryDoc: DiscoveryDoc{
+ // Provider only advertises RS256; HS256 must be rejected.
+ IDTokenSigningAlgValuesSupported: []string{"RS256"},
+ },
+ }
+ provider.JWKS.URL = u1
+ cfg := Config{
+ Enabled: true,
+ pubKeys: pubKeys,
+ arnProviderCfgsMap: map[arn.ARN]*providerCfg{
+ DummyRoleARN: &provider,
+ },
+ ProviderCfgs: map[string]*providerCfg{
+ "1": &provider,
+ },
+ closeRespFn: func(rc io.ReadCloser) {
+ rc.Close()
+ },
+ }
+
+ var claims jwtgo.MapClaims
+ if err = cfg.Validate(t.Context(), DummyRoleARN, token, "", "", claims); err == nil {
+ t.Fatal("expected HS256 token to be rejected when provider advertises only RS256")
+ }
+}
+
func TestJWT(t *testing.T) {
const jsonkey = `{"keys":
[