├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── semantic.yml └── workflows │ ├── azwi-build.yaml │ ├── azwi-e2e.yaml │ ├── chart.yaml │ ├── codecov.yaml │ ├── codeql.yaml │ ├── create-release-pull-request.yaml │ ├── create-release.yaml │ ├── dependency-review.yml │ ├── markdown-link-check.yaml │ ├── markdown.links.config.json │ ├── patch-images.yaml │ ├── publish-images.yaml │ ├── scan-vulns.yaml │ ├── scorecards.yml │ └── website.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .pipelines ├── nightly.yaml ├── pr.yaml └── templates │ ├── publish-logs.yaml │ └── upgrade.yaml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── SECURITY.md ├── charts └── workload-identity-webhook │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── _helpers.tpl │ ├── azure-wi-webhook-admin-serviceaccount.yaml │ ├── azure-wi-webhook-config-configmap.yaml │ ├── azure-wi-webhook-controller-manager-deployment.yaml │ ├── azure-wi-webhook-controller-manager-poddisruptionbudget.yaml │ ├── azure-wi-webhook-manager-role-clusterrole.yaml │ ├── azure-wi-webhook-manager-role-role.yaml │ ├── azure-wi-webhook-manager-rolebinding-clusterrolebinding.yaml │ ├── azure-wi-webhook-manager-rolebinding-rolebinding.yaml │ ├── azure-wi-webhook-mutating-webhook-configuration-mutatingwebhookconfiguration.yaml │ ├── azure-wi-webhook-server-cert-secret.yaml │ └── azure-wi-webhook-webhook-service-service.yaml │ └── values.yaml ├── cmd ├── azwi │ └── main.go ├── proxy │ └── main.go └── webhook │ └── main.go ├── codecov.yml ├── config ├── certmanager │ ├── certificate.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── default │ ├── kustomization.yaml │ ├── manager_auth_proxy_patch.yaml │ ├── manager_webhook_patch.yaml │ └── webhookcainjection_patch.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── pdb │ ├── kustomization.yaml │ └── pdb.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_role_binding.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml ├── secret │ ├── kustomization.yaml │ └── secret.yaml └── webhook │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ ├── manifests.yaml │ ├── service.yaml │ └── webhook_patch.yaml ├── deploy └── azure-wi-webhook.yaml ├── docker ├── proxy-init.Dockerfile ├── proxy.Dockerfile └── webhook.Dockerfile ├── docs └── book │ ├── Makefile │ ├── README.md │ ├── book.toml │ └── src │ ├── SUMMARY.md │ ├── code-of-conduct.md │ ├── concepts.md │ ├── contributing.md │ ├── development.md │ ├── development │ └── releasing.md │ ├── faq.md │ ├── images │ ├── azure-portal-federated-credential-kubernetes.png │ ├── azure-portal-mi-federated-credential.png │ ├── flow-diagram.png │ ├── how-it-works-diagram.png │ ├── oidc-issuer-sequence-diagram.png │ ├── proxy-diagram.png │ ├── release-step-1.png │ ├── release-step-2.png │ ├── release-step-3.png │ └── release-step-4.png │ ├── installation.md │ ├── installation │ ├── azwi.md │ ├── managed-clusters.md │ ├── mutating-admission-webhook.md │ ├── self-managed-clusters.md │ └── self-managed-clusters │ │ ├── configurations.md │ │ ├── oidc-issuer.md │ │ ├── oidc-issuer │ │ ├── discovery-document.md │ │ └── jwks.md │ │ └── service-account-key-generation.md │ ├── introduction.md │ ├── known-issues.md │ ├── quick-start.md │ ├── topics.md │ ├── topics │ ├── azwi.md │ ├── azwi │ │ ├── jwks.md │ │ ├── serviceaccount-create.md │ │ └── serviceaccount-delete.md │ ├── federated-identity-credential.md │ ├── language-specific-examples.md │ ├── language-specific-examples │ │ ├── azure-identity-sdk.md │ │ └── msal.md │ ├── metrics.md │ ├── self-managed-clusters.md │ ├── self-managed-clusters │ │ ├── examples.md │ │ ├── examples │ │ │ └── kind.md │ │ └── service-account-key-rotation.md │ └── service-account-labels-and-annotations.md │ └── troubleshooting.md ├── examples ├── migration │ └── pod-with-proxy-init-and-proxy-sidecar.yaml ├── msal-go │ ├── Dockerfile │ ├── Makefile │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── token_credential.go │ └── windows.Dockerfile ├── msal-java │ ├── Dockerfile │ ├── Makefile │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── example │ │ └── msal │ │ └── java │ │ ├── App.java │ │ └── CustomTokenCredential.java ├── msal-net │ └── akvdotnet │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── Program.cs │ │ ├── TokenCredential.cs │ │ └── akvdotnet.csproj ├── msal-node │ ├── .gitignore │ ├── Dockerfile │ ├── Makefile │ ├── index.js │ ├── package-lock.json │ └── package.json └── msal-python │ ├── Dockerfile │ ├── Makefile │ ├── main.py │ ├── requirements.txt │ └── token_credential.py ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt └── go-install.sh ├── init └── init-iptables.sh ├── manifest_staging ├── charts │ └── workload-identity-webhook │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── README.md │ │ ├── templates │ │ ├── _helpers.tpl │ │ ├── azure-wi-webhook-admin-serviceaccount.yaml │ │ ├── azure-wi-webhook-config-configmap.yaml │ │ ├── azure-wi-webhook-controller-manager-deployment.yaml │ │ ├── azure-wi-webhook-controller-manager-poddisruptionbudget.yaml │ │ ├── azure-wi-webhook-manager-role-clusterrole.yaml │ │ ├── azure-wi-webhook-manager-role-role.yaml │ │ ├── azure-wi-webhook-manager-rolebinding-clusterrolebinding.yaml │ │ ├── azure-wi-webhook-manager-rolebinding-rolebinding.yaml │ │ ├── azure-wi-webhook-mutating-webhook-configuration-mutatingwebhookconfiguration.yaml │ │ ├── azure-wi-webhook-server-cert-secret.yaml │ │ └── azure-wi-webhook-webhook-service-service.yaml │ │ └── values.yaml └── deploy │ └── azure-wi-webhook.yaml ├── netlify.toml ├── pkg ├── cloud │ ├── azureclient.go │ ├── errors.go │ ├── errors_test.go │ ├── graph.go │ ├── graph_test.go │ ├── mock_cloud │ │ ├── cloud_mock.go │ │ └── doc.go │ ├── roleassignments.go │ └── roledefinitions.go ├── cmd │ ├── jwks │ │ ├── root.go │ │ └── root_test.go │ ├── podidentity │ │ ├── detect.go │ │ ├── detect_test.go │ │ ├── k8s │ │ │ ├── cronjob.go │ │ │ ├── daemonset.go │ │ │ ├── deployment.go │ │ │ ├── job.go │ │ │ ├── localobject.go │ │ │ ├── pod.go │ │ │ ├── replicaset.go │ │ │ ├── replicationcontroller.go │ │ │ └── statefulset.go │ │ └── root.go │ ├── root.go │ ├── serviceaccount │ │ ├── auth │ │ │ ├── provider.go │ │ │ └── provider_test.go │ │ ├── create.go │ │ ├── create_test.go │ │ ├── delete.go │ │ ├── delete_test.go │ │ ├── options │ │ │ ├── errors.go │ │ │ ├── errors_test.go │ │ │ └── options.go │ │ ├── phases │ │ │ ├── create │ │ │ │ ├── aadapplication.go │ │ │ │ ├── aadapplication_test.go │ │ │ │ ├── data.go │ │ │ │ ├── data_test.go │ │ │ │ ├── federatedidentitycredential.go │ │ │ │ ├── federatedidentitycredential_test.go │ │ │ │ ├── roleassignment.go │ │ │ │ ├── roleassignment_test.go │ │ │ │ ├── serviceaccount.go │ │ │ │ └── serviceaccount_test.go │ │ │ ├── delete │ │ │ │ ├── aadapplication.go │ │ │ │ ├── aadapplication_test.go │ │ │ │ ├── data.go │ │ │ │ ├── data_test.go │ │ │ │ ├── federatedidentitycredential.go │ │ │ │ ├── federatedidentitycredential_test.go │ │ │ │ ├── roleassignment.go │ │ │ │ ├── roleassignment_test.go │ │ │ │ ├── serviceaccount.go │ │ │ │ └── serviceaccount_test.go │ │ │ └── workflow │ │ │ │ ├── phase.go │ │ │ │ ├── runner.go │ │ │ │ └── runner_test.go │ │ ├── root.go │ │ └── util │ │ │ ├── util.go │ │ │ └── util_test.go │ └── version │ │ ├── root.go │ │ └── root_test.go ├── config │ ├── config.go │ └── config_test.go ├── kuberneteshelper │ ├── azureidentity.go │ ├── azureidentitybinding.go │ ├── azureidentitybinding_test.go │ ├── client.go │ ├── pod.go │ ├── serviceaccount.go │ └── serviceaccount_test.go ├── metrics │ ├── exporter.go │ ├── exporter_test.go │ └── exporters │ │ └── prometheus │ │ └── prometheus.go ├── proxy │ ├── probe.go │ ├── probe_test.go │ ├── proxy.go │ └── proxy_test.go ├── util │ ├── pod_info.go │ └── pod_info_test.go ├── version │ ├── version.go │ └── version_test.go └── webhook │ ├── consts.go │ ├── stats_reporter.go │ ├── webhook.go │ └── webhook_test.go ├── scripts ├── ci-e2e.sh ├── create-aks-cluster.sh ├── create-kind-cluster.sh └── wi-kind-setup.sh ├── test └── e2e │ ├── e2e.go │ ├── e2e_test.go │ ├── go.mod │ ├── go.sum │ ├── helpers.go │ ├── proxy_test.go │ ├── token_exchange.go │ └── webhook.go └── third_party ├── japaric └── trust │ ├── LICENSE │ └── crate_install.sh └── open-policy-agent └── gatekeeper └── helmify ├── LICENSE ├── README.md ├── delete-ports.yaml ├── kustomization.yaml ├── kustomize-for-helm.yaml ├── main.go ├── replacements.go └── static ├── .helmignore ├── Chart.yaml ├── README.md ├── templates └── _helpers.tpl └── values.yaml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | **Steps To Reproduce** 13 | 14 | **Expected behavior** 15 | 16 | **Logs** 17 | 18 | **Environment** 19 | 20 | - Kubernetes version (use `kubectl version`): 21 | - Cloud provider or hardware configuration: 22 | - OS (e.g: `cat /etc/os-release`): 23 | - Kernel (e.g. `uname -a`): 24 | - Install tools: 25 | - Network plugin and version (if this is a network-related bug): 26 | - Others: 27 | 28 | **Additional context** 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | **Describe the solution you'd like** 13 | 14 | **Describe alternatives you've considered** 15 | 16 | **Additional context** 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Reason for Change**: 2 | 3 | 4 | 8 | 9 | 13 | 14 | **Requirements** 15 | 16 | - [ ] squashed commits 17 | - [ ] included documentation 18 | - [ ] added unit tests and e2e tests (if applicable). 19 | 20 | **Issue Fixed**: 21 | 22 | 23 | **Please answer the following questions with yes/no**: 24 | 25 | Does this change contain code from or inspired by another project? If so, did you notify the maintainers and provide attribution? 26 | 27 | - [ ] yes 28 | - [ ] no 29 | 30 | **Notes for Reviewers**: 31 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleOnly: true 2 | types: 3 | - chore 4 | - ci 5 | - docs 6 | - feat 7 | - fix 8 | - perf 9 | - refactor 10 | - release 11 | - revert 12 | - security 13 | - test 14 | -------------------------------------------------------------------------------- /.github/workflows/azwi-build.yaml: -------------------------------------------------------------------------------- 1 | name: Azure Workload Identity CI 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' # nightly 7 | pull_request: 8 | branches: 9 | - main 10 | - release-** 11 | paths-ignore: 12 | - docs/** 13 | - README.md 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | azwi_build: 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | # TODO(aramase): add windows test env 24 | env: [ubuntu-latest, macos-13] 25 | runs-on: ${{ matrix.env }} 26 | steps: 27 | - name: Harden Runner 28 | uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 29 | with: 30 | egress-policy: audit 31 | 32 | - name: Checkout 33 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 34 | with: 35 | fetch-depth: 0 36 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 37 | with: 38 | go-version: "1.23" 39 | check-latest: true 40 | - name: Build azwi 41 | run: | 42 | make bin/azwi 43 | - name: Validate azwi commands 44 | run: | 45 | ./bin/azwi version 46 | ./bin/azwi -h 47 | ./bin/azwi serviceaccount -h 48 | ./bin/azwi serviceaccount create -h 49 | ./bin/azwi serviceaccount delete -h 50 | ./bin/azwi jwks -h 51 | -------------------------------------------------------------------------------- /.github/workflows/azwi-e2e.yaml: -------------------------------------------------------------------------------- 1 | name: Azure Workload Identity E2E 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' # nightly 7 | push: 8 | branches: 9 | - main 10 | - release-** 11 | 12 | permissions: 13 | id-token: write 14 | contents: read 15 | 16 | jobs: 17 | azwi_build: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | # TODO(aramase): add windows test env 22 | env: [ubuntu-latest, macos-13] 23 | runs-on: ${{ matrix.env }} 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 27 | with: 28 | egress-policy: audit 29 | 30 | - name: Checkout 31 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 32 | with: 33 | fetch-depth: 0 34 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 35 | with: 36 | go-version: "1.23" 37 | check-latest: true 38 | - name: Build azwi 39 | run: | 40 | make bin/azwi 41 | - name: Validate azwi commands 42 | run: | 43 | ./bin/azwi version 44 | ./bin/azwi -h 45 | ./bin/azwi serviceaccount -h 46 | ./bin/azwi serviceaccount create -h 47 | ./bin/azwi serviceaccount delete -h 48 | ./bin/azwi jwks -h 49 | -------------------------------------------------------------------------------- /.github/workflows/chart.yaml: -------------------------------------------------------------------------------- 1 | name: publish_helm_chart 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/chart.yaml" 9 | - "charts/**" 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Harden Runner 19 | uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 20 | with: 21 | egress-policy: audit 22 | 23 | - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 24 | with: 25 | submodules: true 26 | fetch-depth: 0 27 | - name: Publish Helm chart 28 | uses: stefanprodan/helm-gh-pages@0ad2bb377311d61ac04ad9eb6f252fb68e207260 # v1.7.0 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | charts_dir: charts 32 | target_dir: charts 33 | linting: off 34 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yaml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | codecov: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Harden Runner 17 | uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 18 | with: 19 | egress-policy: audit 20 | 21 | - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 22 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 23 | with: 24 | go-version: "^1.23" 25 | check-latest: true 26 | - name: Run tests 27 | run: make test 28 | - uses: codecov/codecov-action@7afa10ed9b269c561c2336fd862446844e0cbf71 29 | with: 30 | files: ./cover.out 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: "0 7 * * 1" # Mondays at 7:00 AM 12 | 13 | permissions: read-all 14 | 15 | jobs: 16 | analyze: 17 | name: Analyze 18 | runs-on: ubuntu-latest 19 | permissions: 20 | security-events: write 21 | 22 | steps: 23 | - name: Harden Runner 24 | uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1 25 | with: 26 | egress-policy: audit 27 | 28 | - name: Checkout repository 29 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 30 | 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 33 | with: 34 | languages: go 35 | build-mode: autobuild 36 | queries: security-extended,security-and-quality 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0 40 | -------------------------------------------------------------------------------- /.github/workflows/create-release-pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: create_release_pull_request 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | release_version: 6 | description: 'Which version are we creating a release pull request for?' 7 | required: true 8 | based_on_branch: 9 | description: 'Which branch should we base the release pull request on?' 10 | required: true 11 | default: main 12 | 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | 17 | jobs: 18 | create-release-pull-request: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Harden Runner 22 | uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 23 | with: 24 | egress-policy: audit 25 | 26 | - name: validate version 27 | run: | 28 | echo "${{ github.event.inputs.release_version }}" | grep -E 'v[0-9]+\.[0-9]+\.[0-9]+(-alpha\.[0-9]+|-beta\.[0-9]+|-rc\.[0-9]+)?$' 29 | echo "${{ github.event.inputs.based_on_branch }}" | grep -E '^(main|release-[0-9]+\.[0-9]+)$' 30 | - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 31 | with: 32 | submodules: true 33 | fetch-depth: 0 34 | ref: "${{ github.event.inputs.based_on_branch }}" 35 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 36 | with: 37 | go-version: "1.23" 38 | check-latest: true 39 | - run: make release-manifest 40 | env: 41 | NEW_VERSION: "${{ github.event.inputs.release_version }}" 42 | - run: make promote-staging-manifest 43 | - name: Create release pull request 44 | uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 # v5.0.1 45 | with: 46 | commit-message: "release: update manifest and helm charts for ${{ github.event.inputs.release_version }}" 47 | title: "release: update manifest and helm charts for ${{ github.event.inputs.release_version }}" 48 | branch: "release-${{ github.event.inputs.release_version }}" 49 | base: ${{ github.event.inputs.based_on_branch }} 50 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yaml: -------------------------------------------------------------------------------- 1 | name: create_release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | create-release: 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Cleanup disk 15 | run: | 16 | # Cleaning up unused tools based on the suggested workaround: 17 | # https://github.com/actions/runner-images/issues/2840#issuecomment-790492173 18 | 19 | # Partial cleanup from the suggested workaround. 20 | # If we continue running out of space, we can remove everything listed in the workaround. 21 | sudo rm -rf /usr/share/dotnet 22 | sudo rm -rf "$AGENT_TOOLSDIRECTORY" 23 | 24 | - name: Harden Runner 25 | uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 26 | with: 27 | egress-policy: audit 28 | 29 | - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 30 | with: 31 | submodules: true 32 | fetch-depth: 0 33 | - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 34 | with: 35 | go-version: "1.23" 36 | check-latest: true 37 | - name: Goreleaser 38 | uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 # v4.3.0 39 | with: 40 | version: "~> v2" 41 | args: release --clean --fail-fast --timeout 150m --verbose 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. 5 | # Once installed, if the workflow run is marked as required, 6 | # PRs introducing known-vulnerable packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | dependency-review: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 21 | with: 22 | egress-policy: audit 23 | 24 | - name: 'Checkout Repository' 25 | uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 26 | - name: 'Dependency Review' 27 | uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 28 | -------------------------------------------------------------------------------- /.github/workflows/markdown-link-check.yaml: -------------------------------------------------------------------------------- 1 | name: Check Markdown links 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '0 0 * * *' # nightly 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | markdown-link-check: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Harden Runner 21 | uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 22 | with: 23 | egress-policy: audit 24 | 25 | - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 26 | - uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # v1 27 | with: 28 | # this will only show errors in the output 29 | use-quiet-mode: 'yes' 30 | # this will show detailed HTTP status for checked links 31 | use-verbose-mode: 'yes' 32 | config-file: '.github/workflows/markdown.links.config.json' 33 | -------------------------------------------------------------------------------- /.github/workflows/markdown.links.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "aliveStatusCodes": [ 3 | 200, 4 | 203 5 | ], 6 | "timeout": "5s", 7 | "retryOn429": true, 8 | "retryCount": 5, 9 | "fallbackRetryDelay": "30s" 10 | } 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/website.yaml: -------------------------------------------------------------------------------- 1 | name: generate_website 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/website.yaml" 9 | - "docs/**" 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Harden Runner 19 | uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1 20 | with: 21 | egress-policy: audit 22 | 23 | - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 24 | with: 25 | submodules: true 26 | fetch-depth: 0 27 | - name: Set TOOLS_BIN_DIR and add to PATH 28 | run: | 29 | TOOLS_BIN_DIR="${HOME}/.cargo/bin" 30 | echo "TOOLS_BIN_DIR=${TOOLS_BIN_DIR}" >> ${GITHUB_ENV} 31 | echo "${TOOLS_BIN_DIR}" >> ${GITHUB_PATH} 32 | - name: Build 33 | run: make -C docs/book build 34 | - name: Deploy 35 | uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./docs/book/book 39 | destination_dir: ./docs 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # IDE directories 18 | .vscode/ 19 | .idea/ 20 | 21 | # binary output 22 | bin/ 23 | hack/tools/bin/ 24 | obj/ 25 | _dist/ 26 | sa.key 27 | sa.pub 28 | 29 | # book 30 | docs/book/book/ 31 | 32 | # logs 33 | _artifacts/ 34 | 35 | # Makefile output 36 | .image-* 37 | 38 | # java target from msal-java example 39 | target/ 40 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 20m 3 | go-version: "1.23" 4 | 5 | linters: 6 | disable-all: true 7 | enable: 8 | - errcheck 9 | - errorlint 10 | - goconst 11 | - gocyclo 12 | - gofmt 13 | - goimports 14 | - gosec 15 | - gosimple 16 | - govet 17 | - ineffassign 18 | - misspell 19 | - nakedret 20 | - nilerr 21 | - prealloc 22 | - revive 23 | - staticcheck 24 | - typecheck 25 | - unconvert 26 | - unused 27 | - whitespace 28 | # Run with --fast=false for more extensive checks 29 | fast: true 30 | 31 | issues: 32 | # default: 50 33 | max-issues-per-linter: 0 34 | exclude-rules: 35 | - text: "unused-parameter: parameter '.*' seems to be unused, consider removing or renaming it as _" 36 | linters: 37 | - revive 38 | 39 | linters-settings: 40 | goimports: 41 | local-prefixes: github.com/Azure/azure-workload-identity 42 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # refer to https://goreleaser.com for more options 2 | version: 2 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - go mod vendor 7 | builds: 8 | - id: azwi 9 | dir: cmd/azwi 10 | binary: azwi 11 | env: 12 | - CGO_ENABLED=0 13 | goos: 14 | - darwin 15 | - linux 16 | - windows 17 | goarch: 18 | - amd64 19 | - arm64 20 | ignore: 21 | - goos: windows 22 | goarch: arm64 23 | flags: 24 | - -mod=vendor 25 | ldflags: 26 | - -s 27 | - -w 28 | - -X {{.ModulePath}}/pkg/version.BuildTime={{.Date}} 29 | - -X {{.ModulePath}}/pkg/version.BuildVersion={{.Tag}} 30 | - -X {{.ModulePath}}/pkg/version.Vcs={{.ShortCommit}} 31 | release: 32 | prerelease: auto 33 | header: | 34 | ## {{.Tag}} - {{ time "2006-01-02" }} 35 | extra_files: 36 | - glob: deploy/*.yaml 37 | archives: 38 | - format_overrides: 39 | - goos: windows 40 | format: zip 41 | name_template: "azwi-{{.Tag}}-{{.Os}}-{{.Arch}}" 42 | checksum: 43 | name_template: 'sha256sums.txt' 44 | algorithm: sha256 45 | changelog: 46 | disable: false 47 | groups: 48 | - title: Bug Fixes 🐞 49 | regexp: ^.*fix[(\\w)]*:+.*$ 50 | - title: Build 🏭 51 | regexp: ^.*build[(\\w)]*:+.*$ 52 | - title: Code Refactoring 💎 53 | regexp: ^.*refactor[(\\w)]*:+.*$ 54 | - title: Code Style 🎶 55 | regexp: ^.*style[(\\w)]*:+.*$ 56 | - title: Continuous Integration 💜 57 | regexp: ^.*ci[(\\w)]*:+.*$ 58 | - title: Documentation 📘 59 | regexp: ^.*docs[(\\w)]*:+.*$ 60 | - title: Features 🌈 61 | regexp: ^.*feat[(\\w)]*:+.*$ 62 | - title: Maintenance 🔧 63 | regexp: ^.*chore[(\\w)]*:+.*$ 64 | - title: Performance Improvements 🚀 65 | regexp: ^.*perf[(\\w)]*:+.*$ 66 | - title: Revert Change ◀️ 67 | regexp: ^.*revert[(\\w)]*:+.*$ 68 | - title: Security Fix 🛡️ 69 | regexp: ^.*security[(\\w)]*:+.*$ 70 | - title: Testing 💚 71 | regexp: ^.*test[(\\w)]*:+.*$ 72 | -------------------------------------------------------------------------------- /.pipelines/templates/publish-logs.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: log_path 3 | type: string 4 | default: _artifacts 5 | 6 | steps: 7 | - task: PublishTestResults@2 8 | inputs: 9 | testResultsFormat: JUnit 10 | testResultsFiles: "_artifacts/**/junit*.xml" 11 | displayName: Publish test results 12 | condition: always() 13 | - task: PublishBuildArtifacts@1 14 | inputs: 15 | pathToPublish: ${{ parameters.log_path }} 16 | artifactName: artifacts/$(Agent.JobName) 17 | displayName: Publish logs 18 | condition: always() 19 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Ref: https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-on-github/about-code-owners 2 | 3 | * @aramase @enj 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: azure-workload-identity.io 2 | repo: github.com/Azure/azure-workload-identity 3 | version: "2" 4 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: workload-identity-webhook 3 | description: A Helm chart to install the azure-workload-identity webhook 4 | type: application 5 | version: 1.5.0 6 | appVersion: v1.5.0 7 | home: https://github.com/Azure/azure-workload-identity 8 | sources: 9 | - https://github.com/Azure/azure-workload-identity 10 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "workload-identity-webhook.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "workload-identity-webhook.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "workload-identity-webhook.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "workload-identity-webhook.labels" -}} 37 | helm.sh/chart: {{ include "workload-identity-webhook.chart" . }} 38 | {{ include "workload-identity-webhook.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "workload-identity-webhook.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "workload-identity-webhook.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Adds the pod labels. 55 | */}} 56 | {{- define "workload-identity-webhook.podLabels" -}} 57 | {{- if .Values.podLabels }} 58 | {{- toYaml .Values.podLabels | nindent 8 }} 59 | {{- end }} 60 | {{- end }} 61 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-admin-serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.imagePullSecrets }} 2 | imagePullSecrets: 3 | {{- toYaml .Values.imagePullSecrets | nindent 2 }} 4 | {{- end }} 5 | apiVersion: v1 6 | kind: ServiceAccount 7 | metadata: 8 | labels: 9 | app: '{{ template "workload-identity-webhook.name" . }}' 10 | azure-workload-identity.io/system: "true" 11 | chart: '{{ template "workload-identity-webhook.name" . }}' 12 | release: '{{ .Release.Name }}' 13 | name: azure-wi-webhook-admin 14 | namespace: '{{ .Release.Namespace }}' 15 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-config-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | AZURE_ENVIRONMENT: {{ .Values.azureEnvironment | default "AzurePublicCloud" }} 4 | AZURE_TENANT_ID: {{ required "A valid .Values.azureTenantID entry required!" .Values.azureTenantID }} 5 | kind: ConfigMap 6 | metadata: 7 | labels: 8 | app: '{{ template "workload-identity-webhook.name" . }}' 9 | azure-workload-identity.io/system: "true" 10 | chart: '{{ template "workload-identity-webhook.name" . }}' 11 | release: '{{ .Release.Name }}' 12 | name: azure-wi-webhook-config 13 | namespace: '{{ .Release.Namespace }}' 14 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-controller-manager-poddisruptionbudget.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-controller-manager 10 | namespace: '{{ .Release.Namespace }}' 11 | spec: 12 | {{- if .Values.podDisruptionBudget.maxUnavailable }} 13 | maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} 14 | {{- end }} 15 | {{- if .Values.podDisruptionBudget.minAvailable }} 16 | minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} 17 | {{- end }} 18 | selector: 19 | matchLabels: 20 | app: '{{ template "workload-identity-webhook.name" . }}' 21 | azure-workload-identity.io/system: "true" 22 | chart: '{{ template "workload-identity-webhook.name" . }}' 23 | release: '{{ .Release.Name }}' 24 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-manager-role-clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-manager-role 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - serviceaccounts 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | - apiGroups: 20 | - admissionregistration.k8s.io 21 | resources: 22 | - mutatingwebhookconfigurations 23 | verbs: 24 | - get 25 | - list 26 | - update 27 | - watch 28 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-manager-role-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-manager-role 10 | namespace: '{{ .Release.Namespace }}' 11 | rules: 12 | - apiGroups: 13 | - "" 14 | resources: 15 | - secrets 16 | verbs: 17 | - create 18 | - delete 19 | - get 20 | - list 21 | - patch 22 | - update 23 | - watch 24 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-manager-rolebinding-clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-manager-rolebinding 10 | roleRef: 11 | apiGroup: rbac.authorization.k8s.io 12 | kind: ClusterRole 13 | name: azure-wi-webhook-manager-role 14 | subjects: 15 | - kind: ServiceAccount 16 | name: azure-wi-webhook-admin 17 | namespace: '{{ .Release.Namespace }}' 18 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-manager-rolebinding-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-manager-rolebinding 10 | namespace: '{{ .Release.Namespace }}' 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: Role 14 | name: azure-wi-webhook-manager-role 15 | subjects: 16 | - kind: ServiceAccount 17 | name: azure-wi-webhook-admin 18 | namespace: '{{ .Release.Namespace }}' 19 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-mutating-webhook-configuration-mutatingwebhookconfiguration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | annotations: 5 | {{- toYaml .Values.mutatingWebhookAnnotations | nindent 4 }} 6 | labels: 7 | app: '{{ template "workload-identity-webhook.name" . }}' 8 | azure-workload-identity.io/system: "true" 9 | chart: '{{ template "workload-identity-webhook.name" . }}' 10 | release: '{{ .Release.Name }}' 11 | name: azure-wi-webhook-mutating-webhook-configuration 12 | webhooks: 13 | - admissionReviewVersions: 14 | - v1 15 | - v1beta1 16 | clientConfig: 17 | service: 18 | name: azure-wi-webhook-webhook-service 19 | namespace: '{{ .Release.Namespace }}' 20 | path: /mutate-v1-pod 21 | failurePolicy: Fail 22 | matchPolicy: Equivalent 23 | name: mutation.azure-workload-identity.io 24 | namespaceSelector: {{- toYaml .Values.mutatingWebhookNamespaceSelector | nindent 4 }} 25 | objectSelector: 26 | matchLabels: 27 | azure.workload.identity/use: "true" 28 | reinvocationPolicy: IfNeeded 29 | rules: 30 | - apiGroups: 31 | - "" 32 | apiVersions: 33 | - v1 34 | operations: 35 | - CREATE 36 | resources: 37 | - pods 38 | sideEffects: None 39 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-server-cert-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-server-cert 10 | namespace: '{{ .Release.Namespace }}' 11 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/templates/azure-wi-webhook-webhook-service-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-webhook-service 10 | namespace: '{{ .Release.Namespace }}' 11 | spec: 12 | {{- if .Values.service }} 13 | type: {{ .Values.service.type | default "ClusterIP" }} 14 | {{- end }} 15 | ports: 16 | - port: 443 17 | targetPort: 9443 18 | selector: 19 | app: '{{ template "workload-identity-webhook.name" . }}' 20 | azure-workload-identity.io/system: "true" 21 | chart: '{{ template "workload-identity-webhook.name" . }}' 22 | release: '{{ .Release.Name }}' 23 | -------------------------------------------------------------------------------- /charts/workload-identity-webhook/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for workload-identity-webhook. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 2 6 | image: 7 | repository: mcr.microsoft.com/oss/azure/workload-identity/webhook 8 | pullPolicy: IfNotPresent 9 | # Overrides the image tag whose default is the chart appVersion. 10 | release: v1.5.0 11 | imagePullSecrets: [] 12 | nodeSelector: 13 | kubernetes.io/os: linux 14 | resources: 15 | limits: 16 | cpu: 100m 17 | memory: 30Mi 18 | requests: 19 | cpu: 100m 20 | memory: 20Mi 21 | tolerations: [] 22 | affinity: {} 23 | service: 24 | type: ClusterIP 25 | port: 443 26 | targetPort: 9443 27 | azureEnvironment: AzurePublicCloud 28 | azureTenantID: 29 | logLevel: info 30 | metricsAddr: ":8095" 31 | metricsBackend: prometheus 32 | priorityClassName: system-cluster-critical 33 | mutatingWebhookAnnotations: {} 34 | podLabels: {} 35 | podAnnotations: {} 36 | mutatingWebhookNamespaceSelector: {} 37 | # minAvailable and maxUnavailable are mutually exclusive 38 | podDisruptionBudget: 39 | minAvailable: 1 40 | # maxUnavailable: 0 41 | -------------------------------------------------------------------------------- /cmd/azwi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Azure/azure-workload-identity/pkg/cmd" 7 | ) 8 | 9 | func main() { 10 | if err := cmd.NewRootCmd().Execute(); err != nil { 11 | os.Exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cmd/proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "monis.app/mlog" 12 | "sigs.k8s.io/controller-runtime/pkg/manager/signals" 13 | 14 | "github.com/Azure/azure-workload-identity/pkg/proxy" 15 | ) 16 | 17 | var ( 18 | proxyPort int 19 | probe bool 20 | logLevel string 21 | ) 22 | 23 | func main() { 24 | if err := mainErr(); err != nil { 25 | mlog.Fatal(err) 26 | } 27 | } 28 | 29 | func mainErr() error { 30 | defer mlog.Setup()() 31 | 32 | flag.IntVar(&proxyPort, "proxy-port", 8000, "Port for the proxy to listen on") 33 | flag.BoolVar(&probe, "probe", false, "Run a readyz probe on the proxy") 34 | flag.StringVar(&logLevel, "log-level", "", 35 | "In order of increasing verbosity: unset (empty string), info, debug, trace and all.") 36 | flag.Parse() 37 | 38 | if err := mlog.ValidateAndSetLogLevelAndFormatGlobally(signals.SetupSignalHandler(), mlog.LogSpec{ 39 | Level: mlog.LogLevel(logLevel), 40 | Format: mlog.FormatJSON, 41 | }); err != nil { 42 | return fmt.Errorf("invalid --log-level set: %w", err) 43 | } 44 | 45 | // when proxy is run with --probe, it will run a readyz probe on the proxy 46 | // this is used in the postStart lifecycle hook to verify the proxy is ready 47 | // to serve requests 48 | if probe { 49 | if err := proxy.Probe(proxyPort); err != nil { 50 | return fmt.Errorf("failed to probe: %w", err) 51 | } 52 | return nil 53 | } 54 | 55 | ctx := withShutdownSignal(context.Background()) 56 | 57 | p, err := proxy.NewProxy(proxyPort, mlog.New().WithName("proxy")) 58 | if err != nil { 59 | return fmt.Errorf("setup: failed to create proxy: %w", err) 60 | } 61 | if err := p.Run(ctx); err != nil { 62 | return fmt.Errorf("setup: failed to run proxy: %w", err) 63 | } 64 | 65 | return nil 66 | } 67 | 68 | // withShutdownSignal returns a copy of the parent context that will close if 69 | // the process receives termination signals. 70 | func withShutdownSignal(ctx context.Context) context.Context { 71 | signalChan := make(chan os.Signal, 1) 72 | signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT, os.Interrupt) 73 | 74 | nctx, cancel := context.WithCancel(ctx) 75 | 76 | go func() { 77 | <-signalChan 78 | mlog.Info("received shutdown signal") 79 | cancel() 80 | }() 81 | return nctx 82 | } 83 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | # Commit status https://docs.codecov.io/docs/commit-status are used 3 | # to block PR based on coverage threshold. 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 0% 9 | patch: 10 | default: 11 | informational: true 12 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for 4 | # breaking changes 5 | apiVersion: cert-manager.io/v1alpha2 6 | kind: Issuer 7 | metadata: 8 | name: selfsigned-issuer 9 | namespace: system 10 | spec: 11 | selfSigned: {} 12 | --- 13 | apiVersion: cert-manager.io/v1alpha2 14 | kind: Certificate 15 | metadata: 16 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 17 | namespace: system 18 | spec: 19 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 20 | dnsNames: 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 23 | issuerRef: 24 | kind: Issuer 25 | name: selfsigned-issuer 26 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 27 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: azure-workload-identity-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: azure-wi-webhook- 10 | 11 | # Labels to add to all resources and selectors. 12 | commonLabels: 13 | azure-workload-identity.io/system: "true" 14 | 15 | bases: 16 | # - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | - ../webhook 22 | - ../secret 23 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 24 | # - ../certmanager 25 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 26 | #- ../prometheus 27 | - ../pdb 28 | 29 | patchesStrategicMerge: 30 | # Protect the /metrics endpoint by putting it behind auth. 31 | # If you want your controller-manager to expose the /metrics 32 | # endpoint w/o any authn/z, please comment the following line. 33 | # - manager_auth_proxy_patch.yaml 34 | 35 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 36 | # crd/kustomization.yaml 37 | - manager_webhook_patch.yaml 38 | 39 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 40 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 41 | # 'CERTMANAGER' needs to be enabled to use ca injection 42 | # - webhookcainjection_patch.yaml 43 | 44 | # the following config is for teaching kustomize how to do var substitution 45 | vars: 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | # - name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 48 | # objref: 49 | # kind: Certificate 50 | # group: cert-manager.io 51 | # version: v1alpha2 52 | # name: serving-cert # this name should match the one in certificate.yaml 53 | # fieldref: 54 | # fieldpath: metadata.namespace 55 | # - name: CERTIFICATE_NAME 56 | # objref: 57 | # kind: Certificate 58 | # group: cert-manager.io 59 | # version: v1alpha2 60 | # name: serving-cert # this name should match the one in certificate.yaml 61 | - name: SERVICE_NAMESPACE # namespace of the service 62 | objref: 63 | kind: Service 64 | version: v1 65 | name: webhook-service 66 | fieldref: 67 | fieldpath: metadata.namespace 68 | - name: SERVICE_NAME 69 | objref: 70 | kind: Service 71 | version: v1 72 | name: webhook-service 73 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | - containerPort: 8095 16 | name: metrics 17 | protocol: TCP 18 | volumeMounts: 19 | - mountPath: /certs 20 | name: cert 21 | readOnly: true 22 | volumes: 23 | - name: cert 24 | secret: 25 | defaultMode: 420 26 | secretName: azure-wi-webhook-server-cert 27 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: manager 7 | newName: mcr.microsoft.com/oss/azure/workload-identity/webhook 8 | newTag: v1.5.0 9 | configMapGenerator: 10 | - literals: 11 | - AZURE_TENANT_ID="${AZURE_TENANT_ID}" 12 | - AZURE_ENVIRONMENT="${AZURE_ENVIRONMENT:-AzurePublicCloud}" 13 | name: config 14 | generatorOptions: 15 | disableNameSuffixHash: true 16 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | pod-security.kubernetes.io/warn: restricted 6 | pod-security.kubernetes.io/warn-version: latest 7 | pod-security.kubernetes.io/audit: restricted 8 | pod-security.kubernetes.io/audit-version: latest 9 | pod-security.kubernetes.io/enforce: restricted 10 | pod-security.kubernetes.io/enforce-version: latest 11 | name: system 12 | --- 13 | apiVersion: apps/v1 14 | kind: Deployment 15 | metadata: 16 | name: controller-manager 17 | namespace: system 18 | labels: {} 19 | spec: 20 | selector: 21 | matchLabels: {} 22 | replicas: 2 23 | template: 24 | metadata: 25 | labels: {} 26 | spec: 27 | serviceAccountName: admin 28 | containers: 29 | - command: 30 | - /manager 31 | args: 32 | - --log-level=info 33 | image: manager:latest 34 | imagePullPolicy: IfNotPresent 35 | name: manager 36 | securityContext: 37 | allowPrivilegeEscalation: false 38 | readOnlyRootFilesystem: true 39 | runAsUser: 65532 40 | runAsGroup: 65532 41 | runAsNonRoot: true 42 | seccompProfile: 43 | type: RuntimeDefault 44 | capabilities: 45 | drop: 46 | - ALL 47 | ports: 48 | - containerPort: 9440 49 | name: healthz 50 | protocol: TCP 51 | readinessProbe: 52 | httpGet: 53 | path: /readyz 54 | port: healthz 55 | initialDelaySeconds: 5 56 | periodSeconds: 5 57 | livenessProbe: 58 | httpGet: 59 | path: /healthz 60 | port: healthz 61 | initialDelaySeconds: 15 62 | periodSeconds: 20 63 | failureThreshold: 6 64 | resources: 65 | limits: 66 | cpu: 100m 67 | memory: 30Mi 68 | requests: 69 | cpu: 100m 70 | memory: 20Mi 71 | env: 72 | - name: POD_NAMESPACE 73 | valueFrom: 74 | fieldRef: 75 | apiVersion: v1 76 | fieldPath: metadata.namespace 77 | envFrom: 78 | - configMapRef: 79 | name: azure-wi-webhook-config 80 | nodeSelector: 81 | kubernetes.io/os: linux 82 | priorityClassName: system-cluster-critical 83 | -------------------------------------------------------------------------------- /config/pdb/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - pdb.yaml 3 | -------------------------------------------------------------------------------- /config/pdb/pdb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | name: controller-manager 5 | spec: 6 | minAvailable: 1 7 | selector: 8 | matchLabels: 9 | azure-workload-identity.io/system: "true" 10 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: {} 7 | name: controller-manager-metrics-monitor 8 | namespace: system 9 | spec: 10 | endpoints: 11 | - path: /metrics 12 | port: https 13 | selector: 14 | matchLabels: {} 15 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: admin 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: {} 5 | name: controller-manager-metrics-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - name: https 10 | port: 8443 11 | targetPort: https 12 | selector: {} 13 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - service_account.yaml 5 | # - leader_election_role.yaml 6 | # - leader_election_role_binding.yaml 7 | # Comment the following 4 lines if you want to disable 8 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 9 | # which protects your /metrics endpoint. 10 | # - auth_proxy_service.yaml 11 | # - auth_proxy_role.yaml 12 | # - auth_proxy_role_binding.yaml 13 | # - auth_proxy_client_clusterrole.yaml 14 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: admin 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - serviceaccounts 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - admissionregistration.k8s.io 17 | resources: 18 | - mutatingwebhookconfigurations 19 | verbs: 20 | - get 21 | - list 22 | - update 23 | - watch 24 | --- 25 | apiVersion: rbac.authorization.k8s.io/v1 26 | kind: Role 27 | metadata: 28 | name: manager-role 29 | namespace: azure-workload-identity-system 30 | rules: 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - secrets 35 | verbs: 36 | - create 37 | - delete 38 | - get 39 | - list 40 | - patch 41 | - update 42 | - watch 43 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: admin 12 | namespace: system 13 | --- 14 | apiVersion: rbac.authorization.k8s.io/v1 15 | kind: RoleBinding 16 | metadata: 17 | name: manager-rolebinding 18 | roleRef: 19 | apiGroup: rbac.authorization.k8s.io 20 | kind: Role 21 | name: manager-role 22 | subjects: 23 | - kind: ServiceAccount 24 | name: admin 25 | namespace: system 26 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: admin 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/secret/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - secret.yaml 3 | -------------------------------------------------------------------------------- /config/secret/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: server-cert 5 | namespace: system 6 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | 8 | patchesStrategicMerge: 9 | - webhook_patch.yaml 10 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | name: mutating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | - v1beta1 10 | clientConfig: 11 | service: 12 | name: webhook-service 13 | namespace: system 14 | path: /mutate-v1-pod 15 | failurePolicy: Fail 16 | matchPolicy: Equivalent 17 | name: mutation.azure-workload-identity.io 18 | reinvocationPolicy: IfNeeded 19 | rules: 20 | - apiGroups: 21 | - "" 22 | apiVersions: 23 | - v1 24 | operations: 25 | - CREATE 26 | resources: 27 | - pods 28 | sideEffects: None 29 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: webhook-service 5 | namespace: system 6 | spec: 7 | ports: 8 | - port: 443 9 | targetPort: 9443 10 | selector: {} 11 | -------------------------------------------------------------------------------- /config/webhook/webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | creationTimestamp: null 5 | name: mutating-webhook-configuration 6 | webhooks: 7 | - name: mutation.azure-workload-identity.io 8 | objectSelector: 9 | matchLabels: 10 | azure.workload.identity/use: "true" 11 | -------------------------------------------------------------------------------- /docker/proxy-init.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=${TARGETPLATFORM:-linux/amd64} registry.k8s.io/build-image/distroless-iptables:v0.7.5 2 | 3 | COPY ./init/init-iptables.sh /bin/ 4 | RUN chmod +x /bin/init-iptables.sh 5 | # Kubernetes runAsNonRoot requires USER to be numeric 6 | USER 65532:65532 7 | 8 | ENTRYPOINT ["./bin/init-iptables.sh"] 9 | -------------------------------------------------------------------------------- /docker/proxy.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/oss/go/microsoft/golang:1.23.8-bookworm@sha256:e052bd5581c75956d08a78b47ba5d12b746aef722f4cd577c98fb571e3072188 as builder 2 | 3 | ARG LDFLAGS 4 | 5 | WORKDIR /workspace 6 | # Copy the Go Modules manifests 7 | COPY go.mod go.mod 8 | COPY go.sum go.sum 9 | # cache deps before building and copying source so that we don't need to re-download as much 10 | # and so that source changes don't invalidate our downloaded layer 11 | RUN go mod download 12 | 13 | # Copy the go source 14 | COPY cmd/proxy/main.go main.go 15 | COPY pkg/ pkg/ 16 | 17 | # Build 18 | ARG TARGETARCH 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} GO111MODULE=on go build -a -ldflags "${LDFLAGS:--X github.com/Azure/azure-workload-identity/pkg/version.BuildVersion=latest}" -o proxy main.go 20 | 21 | FROM --platform=${TARGETPLATFORM:-linux/amd64} mcr.microsoft.com/cbl-mariner/distroless/minimal:2.0-nonroot 22 | WORKDIR / 23 | COPY --from=builder /workspace/proxy . 24 | # Kubernetes runAsNonRoot requires USER to be numeric 25 | USER 1501:1501 26 | 27 | ENTRYPOINT [ "/proxy" ] 28 | -------------------------------------------------------------------------------- /docker/webhook.Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM mcr.microsoft.com/oss/go/microsoft/golang:1.23.8-bookworm@sha256:e052bd5581c75956d08a78b47ba5d12b746aef722f4cd577c98fb571e3072188 as builder 3 | 4 | ARG LDFLAGS 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/webhook/main.go main.go 16 | COPY pkg/ pkg/ 17 | 18 | # Build 19 | ARG TARGETARCH 20 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} GO111MODULE=on go build -a -ldflags "${LDFLAGS:--X github.com/Azure/azure-workload-identity/pkg/version.BuildVersion=latest}" -o manager main.go 21 | 22 | FROM --platform=${TARGETPLATFORM:-linux/amd64} mcr.microsoft.com/cbl-mariner/distroless/minimal:2.0-nonroot 23 | WORKDIR / 24 | COPY --from=builder /workspace/manager . 25 | # Kubernetes runAsNonRoot requires USER to be numeric 26 | USER 65532:65532 27 | 28 | ENTRYPOINT ["/manager"] 29 | -------------------------------------------------------------------------------- /docs/book/Makefile: -------------------------------------------------------------------------------- 1 | TOOLS_BIN_DIR ?= $(PWD)/bin 2 | CRATE_INSTALL := $(realpath ../../third_party/japaric/trust/crate_install.sh) 3 | 4 | MDBOOK := $(TOOLS_BIN_DIR)/mdbook 5 | $(MDBOOK): 6 | $(CRATE_INSTALL) --git rust-lang/mdBook --tag v0.4.10 --to $(TOOLS_BIN_DIR) --force 7 | 8 | MDBOOK_TOC := $(TOOLS_BIN_DIR)/mdbook-toc 9 | $(MDBOOK_TOC): 10 | $(CRATE_INSTALL) --git badboy/mdbook-toc --tag 0.7.0 --to $(TOOLS_BIN_DIR) --force 11 | 12 | DEPS := $(MDBOOK) $(MDBOOK_TOC) 13 | 14 | .PHONY: build 15 | build: $(DEPS) 16 | $(MDBOOK) build 17 | 18 | .PHONY: serve 19 | serve: $(DEPS) 20 | $(MDBOOK) serve 21 | -------------------------------------------------------------------------------- /docs/book/README.md: -------------------------------------------------------------------------------- 1 | # Azure AD Workload Identity Book 2 | 3 | This directory includes the source code for https://azure.github.io/azure-workload-identity/. 4 | 5 | ## Development 6 | 7 | ```bash 8 | make serve 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Ernest Wong"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Azure AD Workload Identity" 7 | 8 | [output.html] 9 | curly-quotes = true 10 | git-repository-url = "https://github.com/Azure/azure-workload-identity" 11 | 12 | [preprocessor.toc] 13 | command = "mdbook-toc" 14 | -------------------------------------------------------------------------------- /docs/book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](./introduction.md) 4 | - [Installation](./installation.md) 5 | - [Managed Clusters](./installation/managed-clusters.md) 6 | - [Self-Managed Clusters](./installation/self-managed-clusters.md) 7 | - [Service Account Key Generation](./installation/self-managed-clusters/service-account-key-generation.md) 8 | - [OpenID Connect Issuer](./installation/self-managed-clusters/oidc-issuer.md) 9 | - [Discovery Document](./installation/self-managed-clusters/oidc-issuer/discovery-document.md) 10 | - [JSON Web Key Sets (JWKS)](./installation/self-managed-clusters/oidc-issuer/jwks.md) 11 | - [Configurations](./installation/self-managed-clusters/configurations.md) 12 | - [Mutating Admission Webhook](./installation/mutating-admission-webhook.md) 13 | - [Azure AD Workload Identity CLI (`azwi`)](./installation/azwi.md) 14 | - [Quick Start](./quick-start.md) 15 | - [Concepts](./concepts.md) 16 | - [Topics](./topics.md) 17 | - [Service Account Labels And Annotations](./topics/service-account-labels-and-annotations.md) 18 | - [Federated Identity Credential](./topics/federated-identity-credential.md) 19 | - [Azure Workload Identity CLI (`azwi`)](./topics/azwi.md) 20 | - [`azwi serviceaccount create`](./topics/azwi/serviceaccount-create.md) 21 | - [`azwi serviceaccount delete`](./topics/azwi/serviceaccount-delete.md) 22 | - [`azwi jwks`](./topics/azwi/jwks.md) 23 | - [Self-Managed Clusters](./topics/self-managed-clusters.md) 24 | - [Service Account Key Rotation](./topics/self-managed-clusters/service-account-key-rotation.md) 25 | - [Examples](./topics/self-managed-clusters/examples.md) 26 | - [Kubernetes in Docker (kind)](./topics/self-managed-clusters/examples/kind.md) 27 | - [Language-Specific Examples](./topics/language-specific-examples.md) 28 | - [Azure Identity client libraries](./topics/language-specific-examples/azure-identity-sdk.md) 29 | - [Microsoft Authentication Library (MSAL)](./topics/language-specific-examples/msal.md) 30 | - [Metrics](./topics/metrics.md) 31 | - [Frequently Asked Questions](./faq.md) 32 | - [Troubleshooting](./troubleshooting.md) 33 | - [Known Issues](./known-issues.md) 34 | - [Development](./development.md) 35 | - [Releasing](./development/releasing.md) 36 | - [Contributing](./contributing.md) 37 | - [Code of Conduct](./code-of-conduct.md) 38 | -------------------------------------------------------------------------------- /docs/book/src/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 4 | -------------------------------------------------------------------------------- /docs/book/src/concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | ![Flow Diagram][1] 4 | 5 | ## Service Account 6 | 7 | > "A service account provides an identity for processes that run in a Pod." - [source][2] 8 | 9 | Azure AD Workload Identity supports the following mappings: 10 | 11 | * one-to-one (a service account referencing an AAD object) 12 | * many-to-one (multiple service accounts referencing the same AAD object). 13 | * one-to-many (a service account referencing multiple AAD objects by changing the [client ID annotation][6]). 14 | 15 | > Note: if the service account annotations are updated, you need to restart the pod for the changes to take effect. 16 | 17 | Users who used [aad-pod-identity][3] can think of a service account as an [AzureIdentity][4], except service account is part of the core Kubernetes API, rather than a CRD. This [doc][5] describes a list of available labels and annotations to configure. 18 | 19 | ## Workload Identity Federation 20 | 21 | Using workload identity federation allows you to access Azure Active Directory (Azure AD) protected resources without needing to manage secrets. This [doc][7] describes in detail on workload identity federation works and steps to create, delete, get or update federated identity credentials. 22 | 23 | [1]: ./images/flow-diagram.png 24 | 25 | [2]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ 26 | 27 | [3]: https://github.com/Azure/aad-pod-identity 28 | 29 | [4]: https://azure.github.io/aad-pod-identity/docs/concepts/azureidentity/ 30 | 31 | [5]: ./topics/service-account-labels-and-annotations.md 32 | 33 | [6]: ./topics/service-account-labels-and-annotations.md#annotations 34 | 35 | [7]: https://docs.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation 36 | -------------------------------------------------------------------------------- /docs/book/src/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The Azure AD Workload Identity project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit [https://cla.microsoft.com](https://cla.microsoft.com). 4 | 5 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. 6 | -------------------------------------------------------------------------------- /docs/book/src/development/releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 4 | 5 | Currently, Azure Workload Identity releases on a monthly basis, targeting the last week of the month. 6 | 7 | We use GitHub Actions to automate our release process. 8 | 9 | ## 1. Create a release pull request 10 | 11 | ![Create a release pull request][1] 12 | 13 | ## 2. Review and approve the release pull request 14 | 15 | ![Review and approve the release pull request][2] 16 | 17 | ## 3. Verify that the `create_release` action is triggered after the release pull request is merged 18 | 19 | ![Verify that the create_release action is triggered after the release pull request is merged][3] 20 | 21 | ## 4. Verify that the tag and release is successfully created 22 | 23 | ![Verify that the tag and release is successfully created][4] 24 | 25 | [1]: ../images/release-step-1.png 26 | [2]: ../images/release-step-2.png 27 | [3]: ../images/release-step-3.png 28 | [4]: ../images/release-step-4.png 29 | -------------------------------------------------------------------------------- /docs/book/src/images/azure-portal-federated-credential-kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/azure-portal-federated-credential-kubernetes.png -------------------------------------------------------------------------------- /docs/book/src/images/azure-portal-mi-federated-credential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/azure-portal-mi-federated-credential.png -------------------------------------------------------------------------------- /docs/book/src/images/flow-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/flow-diagram.png -------------------------------------------------------------------------------- /docs/book/src/images/how-it-works-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/how-it-works-diagram.png -------------------------------------------------------------------------------- /docs/book/src/images/oidc-issuer-sequence-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/oidc-issuer-sequence-diagram.png -------------------------------------------------------------------------------- /docs/book/src/images/proxy-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/proxy-diagram.png -------------------------------------------------------------------------------- /docs/book/src/images/release-step-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/release-step-1.png -------------------------------------------------------------------------------- /docs/book/src/images/release-step-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/release-step-2.png -------------------------------------------------------------------------------- /docs/book/src/images/release-step-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/release-step-3.png -------------------------------------------------------------------------------- /docs/book/src/images/release-step-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/docs/book/src/images/release-step-4.png -------------------------------------------------------------------------------- /docs/book/src/installation/azwi.md: -------------------------------------------------------------------------------- 1 | # Azure AD Workload CLI (azwi) 2 | 3 | `azwi` is a utility CLI that helps manage Azure AD Workload Identity and automate error-prone operations: 4 | 5 | * Generate the JWKS document from a list of public keys 6 | * Streamline the creation and deletion of the following resources: 7 | * AAD applications 8 | * Kubernetes service accounts 9 | * Federated identities 10 | * Azure role assignments 11 | 12 | ### GitHub Releases 13 | 14 | You can download `azwi` from our [latest GitHub releases][1]. 15 | 16 | ### Homebrew (MacOS only) 17 | 18 | ```bash 19 | brew install Azure/azure-workload-identity/azwi 20 | ``` 21 | 22 | [1]: https://github.com/Azure/azure-workload-identity/releases 23 | -------------------------------------------------------------------------------- /docs/book/src/installation/self-managed-clusters.md: -------------------------------------------------------------------------------- 1 | # Self-Managed Clusters 2 | 3 | When compared to using managed Kubernetes services like AKS, managing your own Kubernetes cluster provides the most freedom in customizing Kubernetes and your workload. However, there are additional setup required before deploying Azure AD Workload Identity to a self-managed cluster. If you are a cluster administrator, make sure you can perform the following actions: 4 | 5 | 1. [Generate your own service account signing key pair][1] and [rotate it regularly][2] (at least quarterly) 6 | 2. Manually set up your [OIDC issuer URL][3], and upload your [discovery document][4] and [JWKS][5] to a public endpoint 7 | 3. Ability to [configure flags][6] for system-critical pods such as `kube-apiserver` and `kube-controller-manager` 8 | 9 | [1]: ./self-managed-clusters/service-account-key-generation.md 10 | 11 | [2]: ../topics/self-managed-clusters/service-account-key-rotation.md 12 | 13 | [3]: ./self-managed-clusters/oidc-issuer.md 14 | 15 | [4]: ./self-managed-clusters/oidc-issuer/discovery-document.md 16 | 17 | [5]: ./self-managed-clusters/oidc-issuer/jwks.md 18 | 19 | [6]: ./self-managed-clusters/configurations.md 20 | -------------------------------------------------------------------------------- /docs/book/src/installation/self-managed-clusters/oidc-issuer.md: -------------------------------------------------------------------------------- 1 | # OpenID Connect Issuer 2 | 3 | With the Kubernetes cluster acting as a token issuer, Azure Active Directory (AAD) leverages OpenID Connect (OIDC) to discover public signing keys and verify the authenticity of the service account token before exchanging it for an AAD token. Your workload can then consume the AAD token to access Azure cloud resources via the Azure Identity SDKs or the Microsoft Authentication Library (MSAL). 4 | 5 | In the case of self-managed clusters, administrator will have to manually publish the cluster's service account issuer URL, which should comply with the [OpenID specification][4]. The following table describes the required OIDC issuer endpoints for Azure AD Workload Identity: 6 | 7 | | Endpoint | Description | 8 | | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | 9 | | [`{IssuerURL}/.well-known/openid-configuration`][1] | Also known as the OIDC discovery document. This contains the metadata about the issuer's configurations. | 10 | | [`{IssuerURL}/openid/v1/jwks`][2] | This contains the public signing key(s) that AAD uses to verify the authenticity of the service account token. | 11 | 12 | ## Sequence Diagram 13 | 14 | 31 | 32 | ![Sequence Diagram][3] 33 | 34 | [1]: ./oidc-issuer/discovery-document.md 35 | 36 | [2]: ./oidc-issuer/jwks.md 37 | 38 | [3]: ../../images/oidc-issuer-sequence-diagram.png 39 | 40 | [4]: https://openid.net/specs/openid-connect-discovery-1_0.html 41 | -------------------------------------------------------------------------------- /docs/book/src/installation/self-managed-clusters/oidc-issuer/discovery-document.md: -------------------------------------------------------------------------------- 1 | # Discovery Document 2 | 3 | 4 | 5 | OpenID Connect describes a [metadata document][1] that contains the metadata of the issuer. This includes information such as the URLs to use and the location of the service's public signing keys. The following section will walk you through how to set up a secured, public OIDC issuer URL using Azure blob storage and upload a minimal discovery document to the storage account. 6 | 7 | ## Walkthrough 8 | 9 | ### 1. Create an Azure Blob storage account 10 | 11 | ```bash 12 | export RESOURCE_GROUP="oidc-issuer" 13 | export LOCATION="westus2" 14 | az group create --name "${RESOURCE_GROUP}" --location "${LOCATION}" 15 | 16 | export AZURE_STORAGE_ACCOUNT="oidcissuer$(openssl rand -hex 4)" 17 | export AZURE_STORAGE_CONTAINER="oidc-test" 18 | az storage account create --resource-group "${RESOURCE_GROUP}" --name "${AZURE_STORAGE_ACCOUNT}" --allow-blob-public-access true 19 | az storage container create --name "${AZURE_STORAGE_CONTAINER}" --public-access blob 20 | ``` 21 | 22 | ### 2. Generate the discovery document 23 | 24 | ```bash 25 | cat < openid-configuration.json 26 | { 27 | "issuer": "https://${AZURE_STORAGE_ACCOUNT}.blob.core.windows.net/${AZURE_STORAGE_CONTAINER}/", 28 | "jwks_uri": "https://${AZURE_STORAGE_ACCOUNT}.blob.core.windows.net/${AZURE_STORAGE_CONTAINER}/openid/v1/jwks", 29 | "response_types_supported": [ 30 | "id_token" 31 | ], 32 | "subject_types_supported": [ 33 | "public" 34 | ], 35 | "id_token_signing_alg_values_supported": [ 36 | "RS256" 37 | ] 38 | } 39 | EOF 40 | ``` 41 | 42 | ### 3. Upload the discovery document 43 | 44 | ```bash 45 | az storage blob upload \ 46 | --container-name "${AZURE_STORAGE_CONTAINER}" \ 47 | --file openid-configuration.json \ 48 | --name .well-known/openid-configuration 49 | ``` 50 | 51 | ### 4. Verify that the discovery document is publicly accessible 52 | 53 | ```bash 54 | curl -s "https://${AZURE_STORAGE_ACCOUNT}.blob.core.windows.net/${AZURE_STORAGE_CONTAINER}/.well-known/openid-configuration" 55 | ``` 56 | 57 |
58 | Output 59 | 60 | ```json 61 | { 62 | "issuer": "https://.blob.core.windows.net/oidc-test/", 63 | "jwks_uri": "https://.blob.core.windows.net/oidc-test/openid/v1/jwks", 64 | "response_types_supported": [ 65 | "id_token" 66 | ], 67 | "subject_types_supported": [ 68 | "public" 69 | ], 70 | "id_token_signing_alg_values_supported": [ 71 | "RS256" 72 | ] 73 | } 74 | ``` 75 | 76 | [1]: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig 77 | -------------------------------------------------------------------------------- /docs/book/src/installation/self-managed-clusters/oidc-issuer/jwks.md: -------------------------------------------------------------------------------- 1 | # JSON Web Key Sets (JWKS) 2 | 3 | 4 | 5 | The JSON Web Key Sets (JWKS) document contains the public signing key(s) that allows AAD to verify the authenticity of the service account token. 6 | 7 | ## Walkthrough 8 | 9 | > Assuming you have access to your service account signing key pair and followed [the guide][1] on how to create and upload the discovery document to an Azure blob storage account. See [this section][2] on how to generate a minimal signing key pair. 10 | 11 | ### 1. Install `azwi` 12 | 13 | [Installation guide][3] 14 | 15 | ### 2. Generate the JWKS document 16 | 17 | ```bash 18 | azwi jwks --public-keys --output-file jwks.json 19 | ``` 20 | 21 | > If you have multiple public signing keys, you can append additional `--public-keys` flag to the command. 22 | 23 | ### 3. Upload the JWKS document 24 | 25 | ```bash 26 | az storage blob upload \ 27 | --container-name "${AZURE_STORAGE_CONTAINER}" \ 28 | --file jwks.json \ 29 | --name openid/v1/jwks 30 | ``` 31 | 32 | ### 4. Verify that the JWKS document is publicly accessible 33 | 34 | ```bash 35 | curl -s "https://${AZURE_STORAGE_ACCOUNT}.blob.core.windows.net/${AZURE_STORAGE_CONTAINER}/openid/v1/jwks" 36 | ``` 37 | 38 |
39 | Output 40 | 41 | ```json 42 | { 43 | "keys": [ 44 | { 45 | "use": "sig", 46 | "kty": "RSA", 47 | "kid": "Me5VC6i4_4mymFj7T5rcUftFjYX70YoCfSnZB6-nBY4", 48 | "alg": "RS256", 49 | "n": "ywg7HeKIFX3vleVKZHeYoNpuLHIDisnczYXrUdIGCNilCJFA1ymjG2UAADnt_FpYUsCVyKYJTqcxNbK4boNg_P3uK39OAqXabwYrilEZvsVJQKhzn8dXLeqAnM98L8eBpySU208KTsfMkS3Q6lqwurUP7c_a3g_1XRJukz_EmQxg9jLD_fQd5VwPTEo8HJQIFqIxFWzjTkkK5hbcL9Cclkf6RpeRyjh7Vem57Fu-jAlxDUiYiqyieM4OBNm4CQjiqDE8_xOC8viNpHNw542MYVDKSRnYui31lCOj32wBDphczR8BbnrZgbqN3K_zzB3gIjcGbWbbGA5xKJYqSu5uRwN89_CWrT3vGw5RN3XQPSbhGC4smgZkOCw3N9i1b-x-rrd-mRse6F95ONaoslCJUbJvxvDdb5X0P4_CVZRwJvUyP3OJ44ZvwzshA-zilG-QC9E1j2R9DTSMqOJzUuOxS0JIvoboteI1FAByV9KyU948zQRM7r7MMZYBKWIsu6h7", 50 | "e": "AQAB" 51 | } 52 | ] 53 | } 54 | ``` 55 | 56 |
57 | 58 | [1]: ./discovery-document.md 59 | 60 | [2]: ../service-account-key-generation.md 61 | 62 | [3]: ../../azwi.md 63 | -------------------------------------------------------------------------------- /docs/book/src/installation/self-managed-clusters/service-account-key-generation.md: -------------------------------------------------------------------------------- 1 | # Service Account Key Generation 2 | 3 | 4 | 5 | There are two keys in an RSA key pair: a private key and a public key. The RSA private key is used to generate the digital signature and the RSA public key is used to verify them. In the case of service account tokens, they are signed by your private key/signing key before being projected to your workload's volume. Azure Active Directory will then use your public key to verify the signature and ensure that the service account tokens are not malicious. 6 | 7 | This section will show you how to generate an RSA key pair using `openssl`. 8 | 9 | > Feel free to skip this section if you are planning to bring your own keys. 10 | 11 | ## Walkthrough 12 | 13 | ### 1. Generate an RSA private key using `openssl` 14 | 15 | ```bash 16 | openssl genrsa -out sa.key 2048 17 | ``` 18 | 19 |
20 | Output 21 | 22 | ```bash 23 | Generating RSA private key, 2048 bit long modulus 24 | .............................................+++ 25 | .......+++ 26 | e is 65537 (0x10001) 27 | ``` 28 | 29 |
30 | 31 | ### 2. Generate an RSA public key from a private key using `openssl` 32 | 33 | ```bash 34 | openssl rsa -in sa.key -pubout -out sa.pub 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/book/src/topics.md: -------------------------------------------------------------------------------- 1 | # Topics 2 | 3 | This section contains information about enabling and configuring various features with Azure AD Workload Identity. We strongly recommend users to go through each applicable topic in order, from setting up your Kubernetes clusters, performing various tasks for self-managed clusters, to leveraging the webhook in your application to securely access Azure cloud resources. 4 | -------------------------------------------------------------------------------- /docs/book/src/topics/azwi.md: -------------------------------------------------------------------------------- 1 | # Azure Workload Identity CLI (`azwi`) 2 | 3 | The Azure Workload Identity CLI (`azwi`) is a utility CLI that helps manage Azure AD Workload Identity and automate error-prone operations: 4 | 5 | * Generate the JWKS document from a list of public keys 6 | * Streamline the creation and deletion of the following resources: 7 | * AAD applications 8 | * Kubernetes service accounts 9 | * Federated identities 10 | * Azure role assignments 11 | -------------------------------------------------------------------------------- /docs/book/src/topics/azwi/jwks.md: -------------------------------------------------------------------------------- 1 | # `azwi jwks` 2 | 3 | Create JSON Web Key Sets for the service account issuer keys. 4 | 5 | ## Synopsis 6 | 7 | This command provides the ability to generate the JSON Web Key Sets (JWKS) for the service account issuer keys 8 | 9 | azwi jwks [flags] 10 | 11 | ## Options 12 | 13 | -h, --help help for jwks 14 | --output-file string The name of the file to write the JWKS to. If not provided, the default output is stdout 15 | --public-keys strings List of public keys to include in the JWKS 16 | 17 | ## Example 18 | 19 | ```bash 20 | azwi jwks --public-keys sa.pub 21 | ``` 22 | 23 |
24 | Output 25 | 26 | ```json 27 | { 28 | "keys": [ 29 | { 30 | "use": "sig", 31 | "kty": "RSA", 32 | "kid": "Me5VC6i4_4mymFj7T5rcUftFjYX70YoCfSnZB6-nBY4", 33 | "alg": "RS256", 34 | "n": "ywg7HeKIFX3vleVKZHeYoNpuLHIDisnczYXrUdIGCNilCJFA1ymjG2UAADnt_FpYUsCVyKYJTqcxNbK4boNg_P3uK39OAqXabwYrilEZvsVJQKhzn8dXLeqAnM98L8eBpySU208KTsfMkS3Q6lqwurUP7c_a3g_1XRJukz_EmQxg9jLD_fQd5VwPTEo8HJQIFqIxFWzjTkkK5hbcL9Cclkf6RpeRyjh7Vem57Fu-jAlxDUiYiqyieM4OBNm4CQjiqDE8_xOC8viNpHNw542MYVDKSRnYui31lCOj32wBDphczR8BbnrZgbqN3K_zzB3gIjcGbWbbGA5xKJYqSu5uRwN89_CWrT3vGw5RN3XQPSbhGC4smgZkOCw3N9i1b-x-rrd-mRse6F95ONaoslCJUbJvxvDdb5X0P4_CVZRwJvUyP3OJ44ZvwzshA-zilG-QC9E1j2R9DTSMqOJzUuOxS0JIvoboteI1FAByV9KyU948zQRM7r7MMZYBKWIsu6h7", 35 | "e": "AQAB" 36 | } 37 | ] 38 | } 39 | ``` 40 | 41 |
42 | -------------------------------------------------------------------------------- /docs/book/src/topics/language-specific-examples.md: -------------------------------------------------------------------------------- 1 | # Language-Specific Examples 2 | 3 | Azure AD Workload Identity works especially well with the [Azure SDK][1] and the [Microsoft Authentication Library (MSAL)][2]. Your workload can leverage any of these library to seamlessly access Azure cloud resources. This section contains example projects using both libraries in different programming languages. 4 | 5 | > You can swap the demo image used in the [quick start](../quick-start.md#7-deploy-workload) with images built from these example projects. 6 | 7 | 8 | [1]: https://azure.microsoft.com/downloads/ 9 | 10 | [2]: https://docs.microsoft.com/azure/active-directory/develop/msal-overview 11 | -------------------------------------------------------------------------------- /docs/book/src/topics/language-specific-examples/azure-identity-sdk.md: -------------------------------------------------------------------------------- 1 | # Azure Identity client libraries 2 | 3 | For details on Workload Identity support in the Azure Identity client libraries, see [Azure Identity client libraries](https://learn.microsoft.com/azure/aks/workload-identity-overview#azure-identity-client-libraries). 4 | -------------------------------------------------------------------------------- /docs/book/src/topics/language-specific-examples/msal.md: -------------------------------------------------------------------------------- 1 | # Microsoft Authentication Library (MSAL) 2 | 3 | | Language | Library | Image | Example | Has Windows Images | 4 | | --------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------ | 5 | | C# | [microsoft-authentication-library-for-dotnet](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet) | `ghcr.io/azure/azure-workload-identity/msal-net` | [Link](https://github.com/Azure/azure-workload-identity/tree/main/examples/msal-net/akvdotnet) | ✅ | 6 | | Go | [microsoft-authentication-library-for-go](https://github.com/AzureAD/microsoft-authentication-library-for-go) | `ghcr.io/azure/azure-workload-identity/msal-go` | [Link](https://github.com/Azure/azure-workload-identity/tree/main/examples/msal-go) | ✅ | 7 | | Java | [microsoft-authentication-library-for-java](https://github.com/AzureAD/microsoft-authentication-library-for-java) | `ghcr.io/azure/azure-workload-identity/msal-java` | [Link](https://github.com/Azure/azure-workload-identity/tree/main/examples/msal-java) | ❌ | 8 | | JavaScript/TypeScript | [microsoft-authentication-library-for-js](https://github.com/AzureAD/microsoft-authentication-library-for-js) | `ghcr.io/azure/azure-workload-identity/msal-node` | [Link](https://github.com/Azure/azure-workload-identity/tree/main/examples/msal-node) | ❌ | 9 | | Python | [microsoft-authentication-library-for-python](https://github.com/AzureAD/microsoft-authentication-library-for-python) | `ghcr.io/azure/azure-workload-identity/msal-python` | [Link](https://github.com/Azure/azure-workload-identity/tree/main/examples/msal-python) | ❌ | 10 | -------------------------------------------------------------------------------- /docs/book/src/topics/self-managed-clusters.md: -------------------------------------------------------------------------------- 1 | # Self-Managed Clusters 2 | 3 | The following sections contain the best practices when using Azure AD Workload Identity on a self-managed clusters. -------------------------------------------------------------------------------- /docs/book/src/topics/self-managed-clusters/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This section contains examples about setting up a self-managed cluster with the required configurations. 4 | 5 | | Tool | Description | Example | 6 | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | 7 | | [Kubernetes in Docker][1] (kind) | Run local Kubernetes clusters using Docker container. A fast way to create a conformant Kubernetes cluster. Great for local testing and development. | [Link][2] | 8 | 9 | [1]: https://kind.sigs.k8s.io/ 10 | 11 | [2]: ./examples/kind.md 12 | -------------------------------------------------------------------------------- /examples/migration/pod-with-proxy-init-and-proxy-sidecar.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: httpbin-pod 5 | labels: 6 | app: httpbin 7 | spec: 8 | serviceAccountName: workload-identity-sa 9 | initContainers: 10 | - name: init-networking 11 | image: mcr.microsoft.com/oss/azure/workload-identity/proxy-init:v1.5.0 12 | securityContext: 13 | capabilities: 14 | add: 15 | - NET_ADMIN 16 | drop: 17 | - ALL 18 | privileged: true 19 | runAsUser: 0 20 | env: 21 | - name: PROXY_PORT 22 | value: "8000" 23 | containers: 24 | - name: nginx 25 | image: nginx:alpine 26 | ports: 27 | - containerPort: 80 28 | - name: proxy 29 | image: mcr.microsoft.com/oss/azure/workload-identity/proxy:v1.5.0 30 | ports: 31 | - containerPort: 8000 32 | -------------------------------------------------------------------------------- /examples/msal-go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/oss/go/microsoft/golang:1.23.8-bookworm@sha256:e052bd5581c75956d08a78b47ba5d12b746aef722f4cd577c98fb571e3072188 as builder 2 | 3 | WORKDIR /workspace 4 | # Copy the Go Modules manifests 5 | COPY go.mod go.mod 6 | COPY go.sum go.sum 7 | # cache deps before building and copying source so that we don't need to re-download as much 8 | # and so that source changes don't invalidate our downloaded layer 9 | RUN go mod download 10 | 11 | # Copy the go source 12 | COPY main.go main.go 13 | COPY token_credential.go token_credential.go 14 | 15 | # Build 16 | ARG TARGETARCH 17 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} GO111MODULE=on go build -a -o msalgo . 18 | 19 | FROM --platform=${TARGETPLATFORM:-linux/amd64} mcr.microsoft.com/cbl-mariner/distroless/minimal:2.0-nonroot 20 | WORKDIR / 21 | COPY --from=builder /workspace/msalgo . 22 | # Kubernetes runAsNonRoot requires USER to be numeric 23 | USER 65532:65532 24 | 25 | ENTRYPOINT ["/msalgo"] 26 | -------------------------------------------------------------------------------- /examples/msal-go/Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY ?= ghcr.io/azure/azure-workload-identity 2 | IMAGE_NAME := msal-go 3 | IMAGE_VERSION ?= latest 4 | 5 | DEMO_IMAGE := $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_VERSION) 6 | 7 | ## -------------------------------------- 8 | ## Images 9 | ## -------------------------------------- 10 | 11 | # Output type of docker buildx build 12 | OUTPUT_TYPE ?= type=registry 13 | 14 | ALL_OS = linux windows 15 | ALL_ARCH.linux = amd64 arm64 16 | ALL_ARCH.windows = amd64 17 | ALL_OSVERSIONS.windows := 1809 ltsc2022 18 | ALL_OS_ARCH.linux = $(foreach arch, ${ALL_ARCH.linux}, linux-$(arch)) 19 | ALL_OS_ARCH.windows = $(foreach osver, ${ALL_OSVERSIONS.windows}, windows-$(osver)-$(foreach arch, ${ALL_ARCH.windows},$(arch))) 20 | ALL_OS_ARCH = $(foreach os, $(ALL_OS), ${ALL_OS_ARCH.${os}}) 21 | 22 | # The architecture of the image 23 | ARCH ?= amd64 24 | # OS Version for the Windows images: 1809, ltsc2022 25 | OSVERSION ?= 1809 26 | 27 | .PHONY: container-linux 28 | container-linux: 29 | docker buildx build \ 30 | --output=$(OUTPUT_TYPE) \ 31 | --platform="linux/$(ARCH)" \ 32 | --tag=$(DEMO_IMAGE)-linux-$(ARCH) . 33 | 34 | .PHONY: container-windows 35 | container-windows: 36 | docker buildx build \ 37 | --build-arg OS_VERSION=$(OSVERSION) \ 38 | --output=$(OUTPUT_TYPE) \ 39 | --platform="windows/$(ARCH)" \ 40 | --file=windows.Dockerfile \ 41 | --tag=$(DEMO_IMAGE)-windows-$(OSVERSION)-$(ARCH) . 42 | 43 | .PHONY: container-all 44 | container-all: 45 | for arch in $(ALL_ARCH.linux); do \ 46 | ARCH=$${arch} $(MAKE) container-linux; \ 47 | done 48 | for osversion in $(ALL_OSVERSIONS.windows); do \ 49 | OSVERSION=$${osversion} $(MAKE) container-windows; \ 50 | done 51 | 52 | .PHONY: push-manifest 53 | push-manifest: 54 | docker manifest create --amend $(DEMO_IMAGE) $(foreach osarch, $(ALL_OS_ARCH), $(DEMO_IMAGE)-${osarch}) 55 | for arch in $(ALL_ARCH.linux); do docker manifest annotate --os linux --arch $${arch} $(DEMO_IMAGE) $(DEMO_IMAGE)-linux-$${arch}; done; \ 56 | set -x; \ 57 | for arch in $(ALL_ARCH.windows); do \ 58 | for osversion in $(ALL_OSVERSIONS.windows); do \ 59 | BASEIMAGE=mcr.microsoft.com/windows/nanoserver:$${osversion}; \ 60 | full_version=`docker manifest inspect $${BASEIMAGE} | jq -r '.manifests[0].platform["os.version"]'`; \ 61 | docker manifest annotate --os windows --arch $${arch} --os-version $${full_version} $(DEMO_IMAGE) $(DEMO_IMAGE)-windows-$${osversion}-$${arch}; \ 62 | done; \ 63 | done; \ 64 | docker manifest push --purge $(DEMO_IMAGE) 65 | -------------------------------------------------------------------------------- /examples/msal-go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Azure/azure-workload-identity/example/msal-go 2 | 3 | go 1.23.8 4 | 5 | require ( 6 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 7 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0 8 | github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 9 | k8s.io/klog/v2 v2.130.1 10 | ) 11 | 12 | require ( 13 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect 14 | github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect 15 | github.com/go-logr/logr v1.4.1 // indirect 16 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 17 | github.com/google/uuid v1.6.0 // indirect 18 | github.com/kylelemons/godebug v1.1.0 // indirect 19 | golang.org/x/net v0.39.0 // indirect 20 | golang.org/x/text v0.24.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /examples/msal-go/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "time" 7 | 8 | "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets" 9 | "k8s.io/klog/v2" 10 | ) 11 | 12 | func main() { 13 | keyvaultURL := os.Getenv("KEYVAULT_URL") 14 | if keyvaultURL == "" { 15 | klog.Fatal("KEYVAULT_URL environment variable is not set") 16 | } 17 | secretName := os.Getenv("SECRET_NAME") 18 | if secretName == "" { 19 | klog.Fatal("SECRET_NAME environment variable is not set") 20 | } 21 | 22 | // Azure AD Workload Identity webhook will inject the following env vars 23 | // AZURE_CLIENT_ID with the clientID set in the service account annotation 24 | // AZURE_TENANT_ID with the tenantID set in the service account annotation. If not defined, then 25 | // the tenantID provided via azure-wi-webhook-config for the webhook will be used. 26 | // AZURE_FEDERATED_TOKEN_FILE is the service account token path 27 | // AZURE_AUTHORITY_HOST is the AAD authority hostname 28 | clientID := os.Getenv("AZURE_CLIENT_ID") 29 | tenantID := os.Getenv("AZURE_TENANT_ID") 30 | tokenFilePath := os.Getenv("AZURE_FEDERATED_TOKEN_FILE") 31 | authorityHost := os.Getenv("AZURE_AUTHORITY_HOST") 32 | 33 | if clientID == "" { 34 | klog.Fatal("AZURE_CLIENT_ID environment variable is not set") 35 | } 36 | if tenantID == "" { 37 | klog.Fatal("AZURE_TENANT_ID environment variable is not set") 38 | } 39 | if tokenFilePath == "" { 40 | klog.Fatal("AZURE_FEDERATED_TOKEN_FILE environment variable is not set") 41 | } 42 | if authorityHost == "" { 43 | klog.Fatal("AZURE_AUTHORITY_HOST environment variable is not set") 44 | } 45 | 46 | cred, err := newClientAssertionCredential(tenantID, clientID, authorityHost, tokenFilePath, nil) 47 | if err != nil { 48 | klog.Fatal(err) 49 | } 50 | 51 | // initialize keyvault client 52 | client, err := azsecrets.NewClient(keyvaultURL, cred, &azsecrets.ClientOptions{}) 53 | if err != nil { 54 | klog.Fatal(err) 55 | } 56 | 57 | for { 58 | secretBundle, err := client.GetSecret(context.Background(), secretName, "", nil) 59 | if err != nil { 60 | klog.ErrorS(err, "failed to get secret from keyvault", "keyvault", keyvaultURL, "secretName", secretName) 61 | os.Exit(1) 62 | } 63 | klog.InfoS("successfully got secret", "secret", *secretBundle.Value) 64 | 65 | // wait for 60 seconds before polling again 66 | time.Sleep(60 * time.Second) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/msal-go/windows.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG SERVERCORE_CACHE=gcr.io/k8s-staging-e2e-test-images/windows-servercore-cache:1.0-linux-amd64-${OS_VERSION:-1809} 2 | ARG BASEIMAGE=mcr.microsoft.com/windows/nanoserver:${OS_VERSION:-1809} 3 | 4 | FROM --platform=linux/amd64 mcr.microsoft.com/oss/go/microsoft/golang:1.23.8-bookworm@sha256:e052bd5581c75956d08a78b47ba5d12b746aef722f4cd577c98fb571e3072188 as builder 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY main.go main.go 16 | COPY token_credential.go token_credential.go 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=windows GO111MODULE=on go build -a -o msalgo.exe . 20 | 21 | FROM --platform=linux/amd64 ${SERVERCORE_CACHE} as core 22 | 23 | FROM --platform=${TARGETPLATFORM:-windows/amd64} ${BASEIMAGE} 24 | WORKDIR / 25 | COPY --from=builder /workspace/msalgo.exe . 26 | COPY --from=core /Windows/System32/netapi32.dll /Windows/System32/netapi32.dll 27 | USER ContainerAdministrator 28 | 29 | ENTRYPOINT [ "/msalgo.exe" ] 30 | -------------------------------------------------------------------------------- /examples/msal-java/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILDER=maven:3.8.4-jdk-11 2 | ARG BASEIMAGE=gcr.io/distroless/java:11-nonroot 3 | 4 | FROM ${BUILDER} as builder 5 | WORKDIR /app 6 | COPY pom.xml . 7 | RUN mvn -e -B dependency:resolve 8 | COPY src ./src 9 | RUN mvn -e -B package 10 | 11 | FROM ${BASEIMAGE} 12 | COPY --from=builder /app/target/msal-java-*.jar /app.jar 13 | # Kubernetes runAsNonRoot requires USER to be numeric 14 | USER 65532:65532 15 | CMD ["/app.jar"] 16 | -------------------------------------------------------------------------------- /examples/msal-java/Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY ?= ghcr.io/azure/azure-workload-identity 2 | IMAGE_NAME := msal-java 3 | IMAGE_VERSION ?= latest 4 | 5 | DEMO_IMAGE := $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_VERSION) 6 | 7 | ## -------------------------------------- 8 | ## Images 9 | ## -------------------------------------- 10 | 11 | # Output type of docker buildx build 12 | OUTPUT_TYPE ?= type=registry 13 | 14 | ALL_OS = linux 15 | ALL_ARCH.linux = amd64 arm64 16 | ALL_OS_ARCH.linux = $(foreach arch, ${ALL_ARCH.linux}, linux-$(arch)) 17 | ALL_OS_ARCH = $(foreach os, $(ALL_OS), ${ALL_OS_ARCH.${os}}) 18 | 19 | # The architecture of the image 20 | ARCH ?= amd64 21 | 22 | .PHONY: container-linux 23 | container-linux: 24 | docker buildx build \ 25 | --output=$(OUTPUT_TYPE) \ 26 | --platform="linux/$(ARCH)" \ 27 | --tag=$(DEMO_IMAGE)-linux-$(ARCH) . 28 | 29 | .PHONY: container-all 30 | container-all: 31 | for arch in $(ALL_ARCH.linux); do \ 32 | ARCH=$${arch} $(MAKE) container-linux; \ 33 | done 34 | 35 | .PHONY: push-manifest 36 | push-manifest: 37 | docker manifest create --amend $(DEMO_IMAGE) $(foreach osarch, $(ALL_OS_ARCH), $(DEMO_IMAGE)-${osarch}) 38 | for arch in $(ALL_ARCH.linux); do docker manifest annotate --os linux --arch $${arch} $(DEMO_IMAGE) $(DEMO_IMAGE)-linux-$${arch}; done; \ 39 | docker manifest push --purge $(DEMO_IMAGE) 40 | -------------------------------------------------------------------------------- /examples/msal-java/src/main/java/com/example/msal/java/App.java: -------------------------------------------------------------------------------- 1 | package com.example.msal.java; 2 | 3 | import java.util.Map; 4 | 5 | import com.azure.security.keyvault.secrets.SecretClient; 6 | import com.azure.security.keyvault.secrets.SecretClientBuilder; 7 | import com.azure.security.keyvault.secrets.models.KeyVaultSecret; 8 | 9 | public class App { 10 | public static void main(String[] args) { 11 | Map env = System.getenv(); 12 | String keyvaultURL = env.get("KEYVAULT_URL"); 13 | if (keyvaultURL == null) { 14 | System.out.println("KEYVAULT_URL environment variable not set"); 15 | return; 16 | } 17 | String secretName = env.get("SECRET_NAME"); 18 | if (secretName == null) { 19 | System.out.println("SECRET_NAME environment variable not set"); 20 | return; 21 | } 22 | 23 | SecretClient secretClient = new SecretClientBuilder() 24 | .vaultUrl(keyvaultURL) 25 | .credential(new CustomTokenCredential()) 26 | .buildClient(); 27 | KeyVaultSecret secret = secretClient.getSecret(secretName); 28 | System.out.printf("successfully got secret, secret=%s", secret.getValue()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/msal-java/src/main/java/com/example/msal/java/CustomTokenCredential.java: -------------------------------------------------------------------------------- 1 | package com.example.msal.java; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.StandardCharsets; 5 | import java.nio.file.Files; 6 | import java.nio.file.Paths; 7 | import java.time.ZoneOffset; 8 | import java.util.HashSet; 9 | import java.util.Map; 10 | import java.util.Set; 11 | 12 | import com.azure.core.credential.AccessToken; 13 | import com.azure.core.credential.TokenCredential; 14 | import com.azure.core.credential.TokenRequestContext; 15 | import com.microsoft.aad.msal4j.ClientCredentialFactory; 16 | import com.microsoft.aad.msal4j.ClientCredentialParameters; 17 | import com.microsoft.aad.msal4j.ConfidentialClientApplication; 18 | import com.microsoft.aad.msal4j.IClientCredential; 19 | 20 | import reactor.core.publisher.Mono; 21 | 22 | public class CustomTokenCredential implements TokenCredential { 23 | private final ConfidentialClientApplication app; 24 | 25 | public CustomTokenCredential() { 26 | Map env = System.getenv(); 27 | String clientAssertion; 28 | try { 29 | clientAssertion = new String(Files.readAllBytes(Paths.get(env.get("AZURE_FEDERATED_TOKEN_FILE"))), 30 | StandardCharsets.UTF_8); 31 | 32 | IClientCredential credential = ClientCredentialFactory.createFromClientAssertion(clientAssertion); 33 | String authority = env.get("AZURE_AUTHORITY_HOST") + env.get("AZURE_TENANT_ID"); 34 | app = ConfidentialClientApplication.builder(env.get("AZURE_CLIENT_ID"), credential) 35 | .authority(authority).build(); 36 | } catch (Exception e) { 37 | System.out.printf("Error creating client application: %s", e.getMessage()); 38 | throw new RuntimeException(e); 39 | } 40 | } 41 | 42 | public Mono getToken(TokenRequestContext request) { 43 | Set scopes = new HashSet<>(); 44 | for (String scope : request.getScopes()) 45 | scopes.add(scope); 46 | 47 | ClientCredentialParameters parameters = ClientCredentialParameters.builder(scopes).build(); 48 | return Mono.defer(() -> Mono.fromFuture(app.acquireToken(parameters))).map((result) -> 49 | new AccessToken(result.accessToken(), result.expiresOnDate().toInstant().atOffset(ZoneOffset.UTC))); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/msal-net/akvdotnet/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:7.0 AS builder 2 | WORKDIR /app 3 | ADD . . 4 | RUN dotnet build akvdotnet.csproj && dotnet publish -c release 5 | 6 | ARG BASEIMAGE 7 | FROM ${BASEIMAGE:-mcr.microsoft.com/dotnet/runtime:5.0} 8 | WORKDIR /app 9 | COPY --from=builder /app/bin/release/netcoreapp5.0/publish/ . 10 | # Kubernetes runAsNonRoot requires USER to be numeric 11 | USER 65532:65532 12 | ENTRYPOINT ["dotnet", "akvdotnet.dll"] 13 | -------------------------------------------------------------------------------- /examples/msal-net/akvdotnet/Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY ?= ghcr.io/azure/azure-workload-identity 2 | IMAGE_NAME := msal-net 3 | IMAGE_VERSION ?= latest 4 | 5 | DEMO_IMAGE := $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_VERSION) 6 | 7 | ## -------------------------------------- 8 | ## Images 9 | ## -------------------------------------- 10 | 11 | # Output type of docker buildx build 12 | OUTPUT_TYPE ?= type=registry 13 | 14 | ALL_OS = linux windows 15 | ALL_ARCH.linux = amd64 arm64 16 | ALL_ARCH.windows = amd64 17 | ALL_OSVERSIONS.windows := 1809 ltsc2022 18 | ALL_OS_ARCH.linux = $(foreach arch, ${ALL_ARCH.linux}, linux-$(arch)) 19 | ALL_OS_ARCH.windows = $(foreach osver, ${ALL_OSVERSIONS.windows}, windows-$(osver)-$(foreach arch, ${ALL_ARCH.windows},$(arch))) 20 | ALL_OS_ARCH = $(foreach os, $(ALL_OS), ${ALL_OS_ARCH.${os}}) 21 | 22 | # The architecture of the image 23 | ARCH ?= amd64 24 | # OS Version for the Windows images: 1809, ltsc2022 25 | OSVERSION ?= 1809 26 | 27 | .PHONY: container-linux 28 | container-linux: 29 | docker buildx build \ 30 | --output=$(OUTPUT_TYPE) \ 31 | --platform="linux/$(ARCH)" \ 32 | --tag=$(DEMO_IMAGE)-linux-$(ARCH) . 33 | 34 | .PHONY: container-windows 35 | container-windows: 36 | docker buildx build \ 37 | --build-arg BASEIMAGE=mcr.microsoft.com/dotnet/runtime:5.0-nanoserver-${OSVERSION} \ 38 | --output=$(OUTPUT_TYPE) \ 39 | --platform="windows/$(ARCH)" \ 40 | --tag=$(DEMO_IMAGE)-windows-$(OSVERSION)-$(ARCH) . 41 | 42 | .PHONY: container-all 43 | container-all: 44 | for arch in $(ALL_ARCH.linux); do \ 45 | ARCH=$${arch} $(MAKE) container-linux; \ 46 | done 47 | for osversion in $(ALL_OSVERSIONS.windows); do \ 48 | OSVERSION=$${osversion} $(MAKE) container-windows; \ 49 | done 50 | 51 | .PHONY: push-manifest 52 | push-manifest: 53 | docker manifest create --amend $(DEMO_IMAGE) $(foreach osarch, $(ALL_OS_ARCH), $(DEMO_IMAGE)-${osarch}) 54 | for arch in $(ALL_ARCH.linux); do docker manifest annotate --os linux --arch $${arch} $(DEMO_IMAGE) $(DEMO_IMAGE)-linux-$${arch}; done; \ 55 | set -x; \ 56 | for arch in $(ALL_ARCH.windows); do \ 57 | for osversion in $(ALL_OSVERSIONS.windows); do \ 58 | BASEIMAGE=mcr.microsoft.com/windows/nanoserver:$${osversion}; \ 59 | full_version=`docker manifest inspect $${BASEIMAGE} | jq -r '.manifests[0].platform["os.version"]'`; \ 60 | docker manifest annotate --os windows --arch $${arch} --os-version $${full_version} $(DEMO_IMAGE) $(DEMO_IMAGE)-windows-$${osversion}-$${arch}; \ 61 | done; \ 62 | done; \ 63 | docker manifest push --purge $(DEMO_IMAGE) 64 | -------------------------------------------------------------------------------- /examples/msal-net/akvdotnet/Program.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using System.Threading; 4 | using Azure.Security.KeyVault.Secrets; 5 | // 6 | 7 | namespace akvdotnet 8 | { 9 | public class Program 10 | { 11 | static void Main(string[] args) 12 | { 13 | Program P = new Program(); 14 | string keyvaultURL = Environment.GetEnvironmentVariable("KEYVAULT_URL"); 15 | if (string.IsNullOrEmpty(keyvaultURL)) { 16 | Console.WriteLine("KEYVAULT_URL environment variable not set"); 17 | return; 18 | } 19 | 20 | string secretName = Environment.GetEnvironmentVariable("SECRET_NAME"); 21 | if (string.IsNullOrEmpty(secretName)) { 22 | Console.WriteLine("SECRET_NAME environment variable not set"); 23 | return; 24 | } 25 | 26 | SecretClient client = new SecretClient( 27 | new Uri(keyvaultURL), 28 | new MyClientAssertionCredential()); 29 | 30 | while (true) 31 | { 32 | Console.WriteLine($"{Environment.NewLine}START {DateTime.UtcNow} ({Environment.MachineName})"); 33 | 34 | // 35 | var keyvaultSecret = client.GetSecret(secretName).Value; 36 | Console.WriteLine("Your secret is " + keyvaultSecret.Value); 37 | 38 | // sleep and retry periodically 39 | Thread.Sleep(600000); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/msal-net/akvdotnet/akvdotnet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/msal-node/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /examples/msal-node/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASEIMAGE=mcr.microsoft.com/mirror/gcr/distroless/nodejs-debian11:16 2 | 3 | FROM mcr.microsoft.com/cbl-mariner/base/nodejs:16 as build-env 4 | ADD . /app 5 | WORKDIR /app 6 | RUN npm install 7 | 8 | FROM ${BASEIMAGE} 9 | COPY --from=build-env /app /app 10 | WORKDIR /app 11 | # Kubernetes runAsNonRoot requires USER to be numeric 12 | USER 65532:65532 13 | CMD ["index.js"] 14 | -------------------------------------------------------------------------------- /examples/msal-node/Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY ?= ghcr.io/azure/azure-workload-identity 2 | IMAGE_NAME := msal-node 3 | IMAGE_VERSION ?= latest 4 | 5 | DEMO_IMAGE := $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_VERSION) 6 | 7 | ## -------------------------------------- 8 | ## Images 9 | ## -------------------------------------- 10 | 11 | # Output type of docker buildx build 12 | OUTPUT_TYPE ?= type=registry 13 | 14 | ALL_OS = linux 15 | ALL_ARCH.linux = amd64 arm64 16 | ALL_OS_ARCH.linux = $(foreach arch, ${ALL_ARCH.linux}, linux-$(arch)) 17 | ALL_OS_ARCH = $(foreach os, $(ALL_OS), ${ALL_OS_ARCH.${os}}) 18 | 19 | # The architecture of the image 20 | ARCH ?= amd64 21 | 22 | .PHONY: container-linux 23 | container-linux: 24 | docker buildx build \ 25 | --output=$(OUTPUT_TYPE) \ 26 | --platform="linux/$(ARCH)" \ 27 | --tag=$(DEMO_IMAGE)-linux-$(ARCH) . 28 | 29 | .PHONY: container-all 30 | container-all: 31 | for arch in $(ALL_ARCH.linux); do \ 32 | ARCH=$${arch} $(MAKE) container-linux; \ 33 | done 34 | 35 | .PHONY: push-manifest 36 | push-manifest: 37 | docker manifest create --amend $(DEMO_IMAGE) $(foreach osarch, $(ALL_OS_ARCH), $(DEMO_IMAGE)-${osarch}) 38 | for arch in $(ALL_ARCH.linux); do docker manifest annotate --os linux --arch $${arch} $(DEMO_IMAGE) $(DEMO_IMAGE)-linux-$${arch}; done; \ 39 | docker manifest push --purge $(DEMO_IMAGE) 40 | -------------------------------------------------------------------------------- /examples/msal-node/index.js: -------------------------------------------------------------------------------- 1 | import msal from "@azure/msal-node" 2 | import fs from "fs" 3 | import { SecretClient } from "@azure/keyvault-secrets" 4 | 5 | class MyClientAssertionCredential { 6 | constructor() { 7 | let clientAssertion = "" 8 | try { 9 | clientAssertion = fs.readFileSync(process.env.AZURE_FEDERATED_TOKEN_FILE, "utf8") 10 | } catch (err) { 11 | console.log("Failed to read client assertion file: " + err) 12 | process.exit(1) 13 | } 14 | 15 | this.app = new msal.ConfidentialClientApplication({ 16 | auth: { 17 | clientId: process.env.AZURE_CLIENT_ID, 18 | authority: `${process.env.AZURE_AUTHORITY_HOST}${process.env.AZURE_TENANT_ID}`, 19 | clientAssertion: clientAssertion, 20 | } 21 | }) 22 | } 23 | 24 | async getToken(scopes) { 25 | const token = await this.app.acquireTokenByClientCredential({ scopes: [scopes] }).catch(error => console.log(error)) 26 | return new Promise((resolve, reject) => { 27 | if (token) { 28 | resolve({ 29 | token: token.accessToken, 30 | expiresOnTimestamp: token.expiresOn.getTime(), 31 | }) 32 | } else { 33 | reject(new Error("Failed to get token silently")) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | const main = async () => { 40 | // create a token credential object, which has a getToken method that returns a token 41 | const tokenCredential = new MyClientAssertionCredential() 42 | 43 | const keyvaultURL = process.env.KEYVAULT_URL 44 | if (!keyvaultURL) { 45 | throw new Error("KEYVAULT_URL environment variable not set") 46 | } 47 | const secretName = process.env.SECRET_NAME 48 | if (!secretName) { 49 | throw new Error("SECRET_NAME environment variable not set") 50 | } 51 | 52 | // create a secret client with the token credential 53 | const keyvault = new SecretClient(keyvaultURL, tokenCredential) 54 | const secret = await keyvault.getSecret(secretName).catch(error => console.log(error)) 55 | console.log(`successfully got secret, secret=${secret.value}`) 56 | } 57 | 58 | main() 59 | -------------------------------------------------------------------------------- /examples/msal-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msal-node", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "type": "module", 10 | "author": "chewong", 11 | "dependencies": { 12 | "@azure/keyvault-secrets": "^4.3.0", 13 | "@azure/msal-node": "^2.0.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/msal-python/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/mirror/docker/library/debian:bookworm-slim AS build 2 | RUN apt-get update && \ 3 | apt-get install --no-install-suggests --no-install-recommends --yes python3-venv gcc libpython3-dev && \ 4 | python3 -m venv /venv && \ 5 | /venv/bin/pip install --upgrade pip setuptools wheel 6 | 7 | # Build the virtualenv as a separate step: Only re-execute this step when requirements.txt changes 8 | FROM build AS build-venv 9 | COPY requirements.txt /requirements.txt 10 | RUN /venv/bin/pip install --disable-pip-version-check -r /requirements.txt 11 | 12 | # Copy the virtualenv into a distroless image 13 | FROM mcr.microsoft.com/cbl-mariner/distroless/python:3.9 14 | COPY --from=build-venv /venv /venv 15 | COPY . /app 16 | WORKDIR /app 17 | # Kubernetes runAsNonRoot requires USER to be numeric 18 | USER 65532:65532 19 | ENTRYPOINT ["/venv/bin/python3", "main.py"] 20 | -------------------------------------------------------------------------------- /examples/msal-python/Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY ?= ghcr.io/azure/azure-workload-identity 2 | IMAGE_NAME := msal-python 3 | IMAGE_VERSION ?= latest 4 | 5 | DEMO_IMAGE := $(REGISTRY)/$(IMAGE_NAME):$(IMAGE_VERSION) 6 | 7 | ## -------------------------------------- 8 | ## Images 9 | ## -------------------------------------- 10 | 11 | # Output type of docker buildx build 12 | OUTPUT_TYPE ?= type=registry 13 | 14 | ALL_OS = linux 15 | ALL_ARCH.linux = amd64 arm64 16 | ALL_OS_ARCH.linux = $(foreach arch, ${ALL_ARCH.linux}, linux-$(arch)) 17 | ALL_OS_ARCH = $(foreach os, $(ALL_OS), ${ALL_OS_ARCH.${os}}) 18 | 19 | # The architecture of the image 20 | ARCH ?= amd64 21 | 22 | .PHONY: container-linux 23 | container-linux: 24 | docker buildx build \ 25 | --output=$(OUTPUT_TYPE) \ 26 | --platform="linux/$(ARCH)" \ 27 | --tag=$(DEMO_IMAGE)-linux-$(ARCH) . 28 | 29 | .PHONY: container-all 30 | container-all: 31 | for arch in $(ALL_ARCH.linux); do \ 32 | ARCH=$${arch} $(MAKE) container-linux; \ 33 | done 34 | 35 | .PHONY: push-manifest 36 | push-manifest: 37 | docker manifest create --amend $(DEMO_IMAGE) $(foreach osarch, $(ALL_OS_ARCH), $(DEMO_IMAGE)-${osarch}) 38 | for arch in $(ALL_ARCH.linux); do docker manifest annotate --os linux --arch $${arch} $(DEMO_IMAGE) $(DEMO_IMAGE)-linux-$${arch}; done; \ 39 | docker manifest push --purge $(DEMO_IMAGE) 40 | -------------------------------------------------------------------------------- /examples/msal-python/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from azure.keyvault.secrets import SecretClient 5 | from token_credential import MyClientAssertionCredential 6 | 7 | def main(): 8 | # get environment variables to authenticate to the key vault 9 | azure_client_id = os.getenv('AZURE_CLIENT_ID', '') 10 | azure_tenant_id = os.getenv('AZURE_TENANT_ID', '') 11 | azure_authority_host = os.getenv('AZURE_AUTHORITY_HOST', '') 12 | azure_federated_token_file = os.getenv('AZURE_FEDERATED_TOKEN_FILE', '') 13 | 14 | # create a token credential object, which has a get_token method that returns a token 15 | token_credential = MyClientAssertionCredential(azure_client_id, azure_tenant_id, azure_authority_host, azure_federated_token_file) 16 | 17 | keyvault_url = os.getenv('KEYVAULT_URL', '') 18 | if not keyvault_url: 19 | raise Exception('KEYVAULT_URL environment variable is not set') 20 | secret_name = os.getenv('SECRET_NAME', '') 21 | if not secret_name: 22 | raise Exception('SECRET_NAME environment variable is not set') 23 | 24 | # create a secret client with the token credential 25 | keyvault = SecretClient(vault_url=keyvault_url, credential=token_credential) 26 | secret = keyvault.get_secret(secret_name) 27 | print('successfully got secret, secret={}'.format(secret.value)) 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /examples/msal-python/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-keyvault-secrets==4.7.0 2 | # MSAL's acquire_token_for_client() comes with built-in cache since 1.23 3 | # And it is safe to upgrade to any new versions in 1.x series 4 | msal>=1.23,<2 5 | -------------------------------------------------------------------------------- /examples/msal-python/token_credential.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from azure.core.credentials import AccessToken 4 | from msal import ConfidentialClientApplication 5 | 6 | # The following code demonstrates the use of the msal library to 7 | # authenticate with a service using client assertion. 8 | class MyClientAssertionCredential(object): 9 | 10 | def __init__(self, azure_client_id, azure_tenant_id, azure_authority_host, azure_federated_token_file): 11 | self.azure_federated_token_file = azure_federated_token_file 12 | # create a confidential client application 13 | self.app = ConfidentialClientApplication( 14 | azure_client_id, 15 | client_credential={ 16 | 'client_assertion': self.read_federation_token, # A callable will be lazily called, whenever a new token is needed 17 | }, 18 | authority="{}{}".format(azure_authority_host, azure_tenant_id) 19 | ) 20 | 21 | def read_federation_token(self): 22 | # read the projected service account token file 23 | with open(self.azure_federated_token_file, 'rb') as f: 24 | return f.read().decode("utf-8") 25 | 26 | def get_token(self, *scopes, **kwargs): 27 | # get the token using the application 28 | token = self.app.acquire_token_for_client(list(scopes)) 29 | if 'error' in token: 30 | raise Exception(token['error_description']) 31 | expires_on = time.time() + token['expires_in'] 32 | # return an access token with the token string and expiration time 33 | return AccessToken(token['access_token'], int(expires_on)) 34 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-workload-identity/b0c4e018378fbea472aa2f82501a96ab681220d1/hack/boilerplate.go.txt -------------------------------------------------------------------------------- /hack/go-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/master/scripts/go_install.sh 3 | 4 | set -o errexit 5 | set -o nounset 6 | set -o pipefail 7 | 8 | if [[ -z "${1}" ]]; then 9 | echo "must provide module as first parameter" 10 | exit 1 11 | fi 12 | 13 | if [[ -z "${2}" ]]; then 14 | echo "must provide binary name as second parameter" 15 | exit 1 16 | fi 17 | 18 | if [[ -z "${3}" ]]; then 19 | echo "must provide version as third parameter" 20 | exit 1 21 | fi 22 | 23 | if [[ -z "${GOBIN}" ]]; then 24 | echo "GOBIN is not set. Must set GOBIN to install the bin in a specified directory." 25 | exit 1 26 | fi 27 | 28 | tmp_dir=$(mktemp -d -t goinstall_XXXXXXXXXX) 29 | function clean { 30 | rm -rf "${tmp_dir}" 31 | } 32 | trap clean EXIT 33 | 34 | rm "${GOBIN}/${2}"* || true 35 | 36 | cd "${tmp_dir}" 37 | 38 | # create a new module in the tmp directory 39 | go mod init fake/mod 40 | 41 | # install the golang module specified as the first argument 42 | go install "${1}@${3}" 43 | mv "${GOBIN}/${2}" "${GOBIN}/${2}-${3}" 44 | ln -sf "${GOBIN}/${2}-${3}" "${GOBIN}/${2}" 45 | -------------------------------------------------------------------------------- /init/init-iptables.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PROXY_PORT=${PROXY_PORT:-8000} 4 | METADATA_IP=${METADATA_IP:-169.254.169.254} 5 | METADATA_PORT=${METADATA_PORT:-80} 6 | PROXY_UID=${PROXY_UID:-1501} 7 | 8 | iptables -t nat -N AZWI_PROXY_OUTPUT 9 | iptables -t nat -N AZWI_PROXY_REDIRECT 10 | 11 | # Redirect all TCP traffic for metatadata endpoint to the proxy 12 | iptables -t nat -A AZWI_PROXY_REDIRECT -p tcp -j REDIRECT --to-port "${PROXY_PORT}" 13 | # For outbound TCP traffic to metadata endpoint on port 80 jump from OUTPUT chain to AZWI_PROXY_OUTPUT chain 14 | iptables -t nat -A OUTPUT -p tcp -d "${METADATA_IP}" --dport "${METADATA_PORT}" -j AZWI_PROXY_OUTPUT 15 | # Skip redirection of proxy traffic back to itself, return to next chain for further processing 16 | iptables -t nat -A AZWI_PROXY_OUTPUT -m owner --uid-owner "${PROXY_UID}" -j ACCEPT 17 | # For all other traffic to metadata point, jump to AZWI_PROXY_REDIRECT chain 18 | iptables -t nat -A AZWI_PROXY_OUTPUT -j AZWI_PROXY_REDIRECT 19 | 20 | # List all iptables rules 21 | iptables -t nat --list 22 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: workload-identity-webhook 3 | description: A Helm chart to install the azure-workload-identity webhook 4 | type: application 5 | version: 1.5.0 6 | appVersion: v1.5.0 7 | home: https://github.com/Azure/azure-workload-identity 8 | sources: 9 | - https://github.com/Azure/azure-workload-identity 10 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "workload-identity-webhook.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "workload-identity-webhook.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "workload-identity-webhook.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "workload-identity-webhook.labels" -}} 37 | helm.sh/chart: {{ include "workload-identity-webhook.chart" . }} 38 | {{ include "workload-identity-webhook.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "workload-identity-webhook.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "workload-identity-webhook.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Adds the pod labels. 55 | */}} 56 | {{- define "workload-identity-webhook.podLabels" -}} 57 | {{- if .Values.podLabels }} 58 | {{- toYaml .Values.podLabels | nindent 8 }} 59 | {{- end }} 60 | {{- end }} 61 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-admin-serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.imagePullSecrets }} 2 | imagePullSecrets: 3 | {{- toYaml .Values.imagePullSecrets | nindent 2 }} 4 | {{- end }} 5 | apiVersion: v1 6 | kind: ServiceAccount 7 | metadata: 8 | labels: 9 | app: '{{ template "workload-identity-webhook.name" . }}' 10 | azure-workload-identity.io/system: "true" 11 | chart: '{{ template "workload-identity-webhook.name" . }}' 12 | release: '{{ .Release.Name }}' 13 | name: azure-wi-webhook-admin 14 | namespace: '{{ .Release.Namespace }}' 15 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-config-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | AZURE_ENVIRONMENT: {{ .Values.azureEnvironment | default "AzurePublicCloud" }} 4 | AZURE_TENANT_ID: {{ required "A valid .Values.azureTenantID entry required!" .Values.azureTenantID }} 5 | kind: ConfigMap 6 | metadata: 7 | labels: 8 | app: '{{ template "workload-identity-webhook.name" . }}' 9 | azure-workload-identity.io/system: "true" 10 | chart: '{{ template "workload-identity-webhook.name" . }}' 11 | release: '{{ .Release.Name }}' 12 | name: azure-wi-webhook-config 13 | namespace: '{{ .Release.Namespace }}' 14 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-controller-manager-poddisruptionbudget.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1 2 | kind: PodDisruptionBudget 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-controller-manager 10 | namespace: '{{ .Release.Namespace }}' 11 | spec: 12 | {{- if .Values.podDisruptionBudget.maxUnavailable }} 13 | maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} 14 | {{- end }} 15 | {{- if .Values.podDisruptionBudget.minAvailable }} 16 | minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} 17 | {{- end }} 18 | selector: 19 | matchLabels: 20 | app: '{{ template "workload-identity-webhook.name" . }}' 21 | azure-workload-identity.io/system: "true" 22 | chart: '{{ template "workload-identity-webhook.name" . }}' 23 | release: '{{ .Release.Name }}' 24 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-manager-role-clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-manager-role 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - serviceaccounts 15 | verbs: 16 | - get 17 | - list 18 | - watch 19 | - apiGroups: 20 | - admissionregistration.k8s.io 21 | resources: 22 | - mutatingwebhookconfigurations 23 | verbs: 24 | - get 25 | - list 26 | - update 27 | - watch 28 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-manager-role-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-manager-role 10 | namespace: '{{ .Release.Namespace }}' 11 | rules: 12 | - apiGroups: 13 | - "" 14 | resources: 15 | - secrets 16 | verbs: 17 | - create 18 | - delete 19 | - get 20 | - list 21 | - patch 22 | - update 23 | - watch 24 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-manager-rolebinding-clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-manager-rolebinding 10 | roleRef: 11 | apiGroup: rbac.authorization.k8s.io 12 | kind: ClusterRole 13 | name: azure-wi-webhook-manager-role 14 | subjects: 15 | - kind: ServiceAccount 16 | name: azure-wi-webhook-admin 17 | namespace: '{{ .Release.Namespace }}' 18 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-manager-rolebinding-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-manager-rolebinding 10 | namespace: '{{ .Release.Namespace }}' 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: Role 14 | name: azure-wi-webhook-manager-role 15 | subjects: 16 | - kind: ServiceAccount 17 | name: azure-wi-webhook-admin 18 | namespace: '{{ .Release.Namespace }}' 19 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-mutating-webhook-configuration-mutatingwebhookconfiguration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | annotations: 5 | {{- toYaml .Values.mutatingWebhookAnnotations | nindent 4 }} 6 | labels: 7 | app: '{{ template "workload-identity-webhook.name" . }}' 8 | azure-workload-identity.io/system: "true" 9 | chart: '{{ template "workload-identity-webhook.name" . }}' 10 | release: '{{ .Release.Name }}' 11 | name: azure-wi-webhook-mutating-webhook-configuration 12 | webhooks: 13 | - admissionReviewVersions: 14 | - v1 15 | - v1beta1 16 | clientConfig: 17 | service: 18 | name: azure-wi-webhook-webhook-service 19 | namespace: '{{ .Release.Namespace }}' 20 | path: /mutate-v1-pod 21 | failurePolicy: Fail 22 | matchPolicy: Equivalent 23 | name: mutation.azure-workload-identity.io 24 | namespaceSelector: {{- toYaml .Values.mutatingWebhookNamespaceSelector | nindent 4 }} 25 | objectSelector: 26 | matchLabels: 27 | azure.workload.identity/use: "true" 28 | reinvocationPolicy: IfNeeded 29 | rules: 30 | - apiGroups: 31 | - "" 32 | apiVersions: 33 | - v1 34 | operations: 35 | - CREATE 36 | resources: 37 | - pods 38 | sideEffects: None 39 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-server-cert-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-server-cert 10 | namespace: '{{ .Release.Namespace }}' 11 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/templates/azure-wi-webhook-webhook-service-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: '{{ template "workload-identity-webhook.name" . }}' 6 | azure-workload-identity.io/system: "true" 7 | chart: '{{ template "workload-identity-webhook.name" . }}' 8 | release: '{{ .Release.Name }}' 9 | name: azure-wi-webhook-webhook-service 10 | namespace: '{{ .Release.Namespace }}' 11 | spec: 12 | {{- if .Values.service }} 13 | type: {{ .Values.service.type | default "ClusterIP" }} 14 | {{- end }} 15 | ports: 16 | - port: 443 17 | targetPort: 9443 18 | selector: 19 | app: '{{ template "workload-identity-webhook.name" . }}' 20 | azure-workload-identity.io/system: "true" 21 | chart: '{{ template "workload-identity-webhook.name" . }}' 22 | release: '{{ .Release.Name }}' 23 | -------------------------------------------------------------------------------- /manifest_staging/charts/workload-identity-webhook/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for workload-identity-webhook. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 2 6 | image: 7 | repository: mcr.microsoft.com/oss/azure/workload-identity/webhook 8 | pullPolicy: IfNotPresent 9 | # Overrides the image tag whose default is the chart appVersion. 10 | release: v1.5.0 11 | imagePullSecrets: [] 12 | nodeSelector: 13 | kubernetes.io/os: linux 14 | resources: 15 | limits: 16 | cpu: 100m 17 | memory: 30Mi 18 | requests: 19 | cpu: 100m 20 | memory: 20Mi 21 | tolerations: [] 22 | affinity: {} 23 | service: 24 | type: ClusterIP 25 | port: 443 26 | targetPort: 9443 27 | azureEnvironment: AzurePublicCloud 28 | azureTenantID: 29 | logLevel: info 30 | metricsAddr: ":8095" 31 | metricsBackend: prometheus 32 | priorityClassName: system-cluster-critical 33 | mutatingWebhookAnnotations: {} 34 | podLabels: {} 35 | podAnnotations: {} 36 | mutatingWebhookNamespaceSelector: {} 37 | # minAvailable and maxUnavailable are mutually exclusive 38 | podDisruptionBudget: 39 | minAvailable: 1 40 | # maxUnavailable: 0 41 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "docs/book/" 3 | publish = "/book" 4 | 5 | [context.deploy-preview] 6 | command = "make build" 7 | -------------------------------------------------------------------------------- /pkg/cloud/graph_test.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import "testing" 4 | 5 | func TestGetDisplayNameFilter(t *testing.T) { 6 | got := getDisplayNameFilter("test") 7 | want := "displayName eq 'test'" 8 | 9 | if got != want { 10 | t.Errorf("getDisplayNameFilter() = %v, want %v", got, want) 11 | } 12 | } 13 | 14 | func TestGetSubjectFilter(t *testing.T) { 15 | got := getSubjectFilter("test") 16 | want := "subject eq 'test'" 17 | 18 | if got != want { 19 | t.Errorf("getSubjectFilter() = %v, want %v", got, want) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/cloud/mock_cloud/doc.go: -------------------------------------------------------------------------------- 1 | // Run go generate to regenerate this mock. 2 | // 3 | //go:generate ../../../hack/tools/bin/mockgen -destination cloud_mock.go -package mock_cloud -source ../azureclient.go 4 | package mock_cloud //nolint 5 | -------------------------------------------------------------------------------- /pkg/cloud/roleassignments.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" 8 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization" 9 | "github.com/google/uuid" 10 | "github.com/pkg/errors" 11 | "monis.app/mlog" 12 | ) 13 | 14 | const ( 15 | roleAssignmentCreateRetryCount = 7 16 | roleAssignmentCreateRetryDelay = 5 * time.Second 17 | ) 18 | 19 | // CreateRoleAssignment creates a role assignment. 20 | func (c *AzureClient) CreateRoleAssignment(ctx context.Context, scope, roleName, principalID string) (armauthorization.RoleAssignment, error) { 21 | var result armauthorization.RoleAssignment 22 | 23 | roleDefinitionID, err := c.GetRoleDefinitionIDByName(ctx, "", roleName) 24 | if err != nil { 25 | return result, errors.Wrapf(err, "failed to get role definition id for role %s", roleName) 26 | } 27 | 28 | mlog.Debug("Creating role assignment", 29 | "principalID", principalID, 30 | "role", roleName, 31 | ) 32 | 33 | parameters := armauthorization.RoleAssignmentCreateParameters{ 34 | Properties: &armauthorization.RoleAssignmentProperties{ 35 | RoleDefinitionID: roleDefinitionID.ID, 36 | PrincipalID: to.Ptr(principalID), 37 | }, 38 | } 39 | 40 | // Adding retries to handle the propagation delay of the service principal. 41 | // Trying to create role assignment immediately after service principal is created 42 | // results in "PrincipalNotFound" error. 43 | for i := 0; i < roleAssignmentCreateRetryCount; i++ { 44 | resp, err := c.roleAssignmentsClient.Create(ctx, scope, uuid.New().String(), parameters, nil) 45 | if err == nil { 46 | return resp.RoleAssignment, nil 47 | } 48 | 49 | if IsRoleAssignmentExists(err) { 50 | mlog.Warning("Role assignment already exists", "principalID", principalID, "role", roleName) 51 | return result, err 52 | } 53 | time.Sleep(roleAssignmentCreateRetryDelay) 54 | } 55 | 56 | return result, err 57 | } 58 | 59 | // DeleteRoleAssignment deletes a role assignment. 60 | func (c *AzureClient) DeleteRoleAssignment(ctx context.Context, roleAssignmentID string) (armauthorization.RoleAssignment, error) { 61 | mlog.Debug("Deleting role assignment", "id", roleAssignmentID) 62 | resp, err := c.roleAssignmentsClient.DeleteByID(ctx, roleAssignmentID, nil) 63 | if err != nil { 64 | return armauthorization.RoleAssignment{}, err 65 | } 66 | return resp.RoleAssignment, nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/cloud/roledefinitions.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization" 8 | "github.com/pkg/errors" 9 | "monis.app/mlog" 10 | ) 11 | 12 | // GetRoleDefinitionIDByName returns the role definition ID for the given role name. 13 | func (c *AzureClient) GetRoleDefinitionIDByName(ctx context.Context, scope, roleName string) (armauthorization.RoleDefinition, error) { 14 | mlog.Debug("Get role definition ID", "name", roleName) 15 | 16 | filter := getRoleNameFilter(roleName) 17 | pager := c.roleDefinitionsClient.NewListPager(scope, &armauthorization.RoleDefinitionsClientListOptions{ 18 | Filter: &filter, 19 | }) 20 | 21 | for pager.More() { 22 | nextResult, err := pager.NextPage(ctx) 23 | if err != nil { 24 | return armauthorization.RoleDefinition{}, errors.Wrap(err, "failed to list role definitions") 25 | } 26 | if len(nextResult.Value) > 0 { 27 | return *nextResult.Value[0], nil 28 | } 29 | } 30 | 31 | return armauthorization.RoleDefinition{}, errors.Errorf("role definition %s not found", roleName) 32 | } 33 | 34 | // getRoleNameFilter returns a filter string for the given role name. 35 | // Supported filters are either roleName eq '{value}' or type eq 'BuiltInRole|CustomRole'." 36 | func getRoleNameFilter(roleName string) string { 37 | return fmt.Sprintf("roleName eq '%s'", roleName) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/k8s/cronjob.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | batchv1 "k8s.io/api/batch/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type cronJobLocalObject struct { 11 | client.Object 12 | } 13 | 14 | func newCronJobLocalObject(obj client.Object) LocalObject { 15 | return &cronJobLocalObject{ 16 | Object: obj, 17 | } 18 | } 19 | 20 | func (o *cronJobLocalObject) GetServiceAccountName() string { 21 | return o.Object.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName 22 | } 23 | 24 | func (o *cronJobLocalObject) SetServiceAccountName(name string) { 25 | o.Object.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName = name 26 | } 27 | 28 | func (o *cronJobLocalObject) GetContainers() []corev1.Container { 29 | return o.Object.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.Containers 30 | } 31 | 32 | func (o *cronJobLocalObject) SetContainers(containers []corev1.Container) { 33 | o.Object.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.Containers = containers 34 | } 35 | 36 | func (o *cronJobLocalObject) GetInitContainers() []corev1.Container { 37 | return o.Object.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.InitContainers 38 | } 39 | 40 | func (o *cronJobLocalObject) SetInitContainers(containers []corev1.Container) { 41 | o.Object.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.InitContainers = containers 42 | } 43 | 44 | func (o *cronJobLocalObject) SetGVK() { 45 | o.Object.(*batchv1.CronJob).SetGroupVersionKind(schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "CronJob"}) 46 | } 47 | 48 | func (o *cronJobLocalObject) ResetStatus() { 49 | o.Object.(*batchv1.CronJob).Status = batchv1.CronJobStatus{} 50 | } 51 | 52 | func (o *cronJobLocalObject) GetObject() client.Object { 53 | return o.Object 54 | } 55 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/k8s/daemonset.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type daemonSetLocalObject struct { 11 | client.Object 12 | } 13 | 14 | func newDaemonSetLocalObject(obj client.Object) LocalObject { 15 | return &daemonSetLocalObject{ 16 | Object: obj, 17 | } 18 | } 19 | 20 | func (o *daemonSetLocalObject) GetServiceAccountName() string { 21 | return o.Object.(*appsv1.DaemonSet).Spec.Template.Spec.ServiceAccountName 22 | } 23 | 24 | func (o *daemonSetLocalObject) SetServiceAccountName(name string) { 25 | o.Object.(*appsv1.DaemonSet).Spec.Template.Spec.ServiceAccountName = name 26 | } 27 | 28 | func (o *daemonSetLocalObject) GetContainers() []corev1.Container { 29 | return o.Object.(*appsv1.DaemonSet).Spec.Template.Spec.Containers 30 | } 31 | 32 | func (o *daemonSetLocalObject) SetContainers(containers []corev1.Container) { 33 | o.Object.(*appsv1.DaemonSet).Spec.Template.Spec.Containers = containers 34 | } 35 | 36 | func (o *daemonSetLocalObject) GetInitContainers() []corev1.Container { 37 | return o.Object.(*appsv1.DaemonSet).Spec.Template.Spec.InitContainers 38 | } 39 | 40 | func (o *daemonSetLocalObject) SetInitContainers(containers []corev1.Container) { 41 | o.Object.(*appsv1.DaemonSet).Spec.Template.Spec.InitContainers = containers 42 | } 43 | 44 | func (o *daemonSetLocalObject) SetGVK() { 45 | o.Object.(*appsv1.DaemonSet).SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "DaemonSet"}) 46 | } 47 | 48 | func (o *daemonSetLocalObject) ResetStatus() { 49 | o.Object.(*appsv1.DaemonSet).Status = appsv1.DaemonSetStatus{} 50 | } 51 | 52 | func (o *daemonSetLocalObject) GetObject() client.Object { 53 | return o.Object 54 | } 55 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/k8s/deployment.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type deploymentLocalObject struct { 11 | client.Object 12 | } 13 | 14 | func newDeploymentLocalObject(obj client.Object) LocalObject { 15 | return &deploymentLocalObject{ 16 | Object: obj, 17 | } 18 | } 19 | 20 | func (o *deploymentLocalObject) GetServiceAccountName() string { 21 | return o.Object.(*appsv1.Deployment).Spec.Template.Spec.ServiceAccountName 22 | } 23 | 24 | func (o *deploymentLocalObject) SetServiceAccountName(name string) { 25 | o.Object.(*appsv1.Deployment).Spec.Template.Spec.ServiceAccountName = name 26 | } 27 | 28 | func (o *deploymentLocalObject) GetContainers() []corev1.Container { 29 | return o.Object.(*appsv1.Deployment).Spec.Template.Spec.Containers 30 | } 31 | 32 | func (o *deploymentLocalObject) SetContainers(containers []corev1.Container) { 33 | o.Object.(*appsv1.Deployment).Spec.Template.Spec.Containers = containers 34 | } 35 | 36 | func (o *deploymentLocalObject) GetInitContainers() []corev1.Container { 37 | return o.Object.(*appsv1.Deployment).Spec.Template.Spec.InitContainers 38 | } 39 | 40 | func (o *deploymentLocalObject) SetInitContainers(containers []corev1.Container) { 41 | o.Object.(*appsv1.Deployment).Spec.Template.Spec.InitContainers = containers 42 | } 43 | 44 | func (o *deploymentLocalObject) SetGVK() { 45 | o.Object.(*appsv1.Deployment).SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}) 46 | } 47 | 48 | func (o *deploymentLocalObject) ResetStatus() { 49 | o.Object.(*appsv1.Deployment).Status = appsv1.DeploymentStatus{} 50 | } 51 | 52 | func (o *deploymentLocalObject) GetObject() client.Object { 53 | return o.Object 54 | } 55 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/k8s/job.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | batchv1 "k8s.io/api/batch/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type jobLocalObject struct { 11 | client.Object 12 | } 13 | 14 | func newJobLocalObject(obj client.Object) LocalObject { 15 | return &jobLocalObject{ 16 | Object: obj, 17 | } 18 | } 19 | 20 | func (o *jobLocalObject) GetServiceAccountName() string { 21 | return o.Object.(*batchv1.Job).Spec.Template.Spec.ServiceAccountName 22 | } 23 | 24 | func (o *jobLocalObject) SetServiceAccountName(name string) { 25 | o.Object.(*batchv1.Job).Spec.Template.Spec.ServiceAccountName = name 26 | } 27 | 28 | func (o *jobLocalObject) GetContainers() []corev1.Container { 29 | return o.Object.(*batchv1.Job).Spec.Template.Spec.Containers 30 | } 31 | 32 | func (o *jobLocalObject) SetContainers(containers []corev1.Container) { 33 | o.Object.(*batchv1.Job).Spec.Template.Spec.Containers = containers 34 | } 35 | 36 | func (o *jobLocalObject) GetInitContainers() []corev1.Container { 37 | return o.Object.(*batchv1.Job).Spec.Template.Spec.InitContainers 38 | } 39 | 40 | func (o *jobLocalObject) SetInitContainers(containers []corev1.Container) { 41 | o.Object.(*batchv1.Job).Spec.Template.Spec.InitContainers = containers 42 | } 43 | 44 | func (o *jobLocalObject) SetGVK() { 45 | o.Object.(*batchv1.Job).SetGroupVersionKind(schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"}) 46 | } 47 | 48 | func (o *jobLocalObject) ResetStatus() { 49 | o.Object.(*batchv1.Job).Status = batchv1.JobStatus{} 50 | } 51 | 52 | func (o *jobLocalObject) GetObject() client.Object { 53 | return o.Object 54 | } 55 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/k8s/localobject.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | batchv1 "k8s.io/api/batch/v1" 6 | corev1 "k8s.io/api/core/v1" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type LocalObject interface { 11 | client.Object 12 | GetServiceAccountName() string 13 | SetServiceAccountName(name string) 14 | GetContainers() []corev1.Container 15 | SetContainers(containers []corev1.Container) 16 | GetInitContainers() []corev1.Container 17 | SetInitContainers(containers []corev1.Container) 18 | SetGVK() 19 | GetObject() client.Object 20 | ResetStatus() 21 | } 22 | 23 | func NewLocalObject(obj client.Object) LocalObject { 24 | switch obj.(type) { 25 | case *corev1.Pod: 26 | return newPodLocalObject(obj) 27 | case *appsv1.Deployment: 28 | return newDeploymentLocalObject(obj) 29 | case *appsv1.StatefulSet: 30 | return newStatefulSetLocalObject(obj) 31 | case *appsv1.DaemonSet: 32 | return newDaemonSetLocalObject(obj) 33 | case *appsv1.ReplicaSet: 34 | return newReplicaSetLocalObject(obj) 35 | case *corev1.ReplicationController: 36 | return newReplicationControllerLocalObject(obj) 37 | case *batchv1.CronJob: 38 | return newCronJobLocalObject(obj) 39 | case *batchv1.Job: 40 | return newJobLocalObject(obj) 41 | default: 42 | return nil 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/k8s/pod.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type podLocalObject struct { 10 | client.Object 11 | } 12 | 13 | func newPodLocalObject(obj client.Object) LocalObject { 14 | return &podLocalObject{ 15 | Object: obj, 16 | } 17 | } 18 | 19 | func (o *podLocalObject) GetServiceAccountName() string { 20 | return o.Object.(*corev1.Pod).Spec.ServiceAccountName 21 | } 22 | 23 | func (o *podLocalObject) SetServiceAccountName(name string) { 24 | o.Object.(*corev1.Pod).Spec.ServiceAccountName = name 25 | } 26 | 27 | func (o *podLocalObject) GetContainers() []corev1.Container { 28 | return o.Object.(*corev1.Pod).Spec.Containers 29 | } 30 | 31 | func (o *podLocalObject) SetContainers(containers []corev1.Container) { 32 | o.Object.(*corev1.Pod).Spec.Containers = containers 33 | } 34 | 35 | func (o *podLocalObject) GetInitContainers() []corev1.Container { 36 | return o.Object.(*corev1.Pod).Spec.InitContainers 37 | } 38 | 39 | func (o *podLocalObject) SetInitContainers(containers []corev1.Container) { 40 | o.Object.(*corev1.Pod).Spec.InitContainers = containers 41 | } 42 | 43 | func (o *podLocalObject) SetGVK() { 44 | o.Object.(*corev1.Pod).SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}) 45 | } 46 | 47 | func (o *podLocalObject) ResetStatus() { 48 | o.Object.(*corev1.Pod).Status = corev1.PodStatus{} 49 | } 50 | 51 | func (o *podLocalObject) GetObject() client.Object { 52 | return o.Object 53 | } 54 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/k8s/replicaset.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type replicaSetLocalObject struct { 11 | client.Object 12 | } 13 | 14 | func newReplicaSetLocalObject(obj client.Object) LocalObject { 15 | return &replicaSetLocalObject{ 16 | Object: obj, 17 | } 18 | } 19 | 20 | func (o *replicaSetLocalObject) GetServiceAccountName() string { 21 | return o.Object.(*appsv1.ReplicaSet).Spec.Template.Spec.ServiceAccountName 22 | } 23 | 24 | func (o *replicaSetLocalObject) SetServiceAccountName(name string) { 25 | o.Object.(*appsv1.ReplicaSet).Spec.Template.Spec.ServiceAccountName = name 26 | } 27 | 28 | func (o *replicaSetLocalObject) GetContainers() []corev1.Container { 29 | return o.Object.(*appsv1.ReplicaSet).Spec.Template.Spec.Containers 30 | } 31 | 32 | func (o *replicaSetLocalObject) SetContainers(containers []corev1.Container) { 33 | o.Object.(*appsv1.ReplicaSet).Spec.Template.Spec.Containers = containers 34 | } 35 | 36 | func (o *replicaSetLocalObject) GetInitContainers() []corev1.Container { 37 | return o.Object.(*appsv1.ReplicaSet).Spec.Template.Spec.InitContainers 38 | } 39 | 40 | func (o *replicaSetLocalObject) SetInitContainers(containers []corev1.Container) { 41 | o.Object.(*appsv1.ReplicaSet).Spec.Template.Spec.InitContainers = containers 42 | } 43 | 44 | func (o *replicaSetLocalObject) SetGVK() { 45 | o.Object.(*appsv1.ReplicaSet).SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ReplicaSet"}) 46 | } 47 | 48 | func (o *replicaSetLocalObject) ResetStatus() { 49 | o.Object.(*appsv1.ReplicaSet).Status = appsv1.ReplicaSetStatus{} 50 | } 51 | 52 | func (o *replicaSetLocalObject) GetObject() client.Object { 53 | return o.Object 54 | } 55 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/k8s/replicationcontroller.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type replicationControllerLocalObject struct { 10 | client.Object 11 | } 12 | 13 | func newReplicationControllerLocalObject(obj client.Object) LocalObject { 14 | return &replicationControllerLocalObject{ 15 | Object: obj, 16 | } 17 | } 18 | 19 | func (o *replicationControllerLocalObject) GetServiceAccountName() string { 20 | return o.Object.(*corev1.ReplicationController).Spec.Template.Spec.ServiceAccountName 21 | } 22 | 23 | func (o *replicationControllerLocalObject) SetServiceAccountName(name string) { 24 | o.Object.(*corev1.ReplicationController).Spec.Template.Spec.ServiceAccountName = name 25 | } 26 | 27 | func (o *replicationControllerLocalObject) GetContainers() []corev1.Container { 28 | return o.Object.(*corev1.ReplicationController).Spec.Template.Spec.Containers 29 | } 30 | 31 | func (o *replicationControllerLocalObject) SetContainers(containers []corev1.Container) { 32 | o.Object.(*corev1.ReplicationController).Spec.Template.Spec.Containers = containers 33 | } 34 | 35 | func (o *replicationControllerLocalObject) GetInitContainers() []corev1.Container { 36 | return o.Object.(*corev1.ReplicationController).Spec.Template.Spec.InitContainers 37 | } 38 | 39 | func (o *replicationControllerLocalObject) SetInitContainers(containers []corev1.Container) { 40 | o.Object.(*corev1.ReplicationController).Spec.Template.Spec.InitContainers = containers 41 | } 42 | 43 | func (o *replicationControllerLocalObject) SetGVK() { 44 | o.Object.(*corev1.ReplicationController).SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ReplicationController"}) 45 | } 46 | 47 | func (o *replicationControllerLocalObject) ResetStatus() { 48 | o.Object.(*corev1.ReplicationController).Status = corev1.ReplicationControllerStatus{} 49 | } 50 | 51 | func (o *replicationControllerLocalObject) GetObject() client.Object { 52 | return o.Object 53 | } 54 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/k8s/statefulset.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type statefulSetLocalObject struct { 11 | client.Object 12 | } 13 | 14 | func newStatefulSetLocalObject(obj client.Object) LocalObject { 15 | return &statefulSetLocalObject{ 16 | Object: obj, 17 | } 18 | } 19 | 20 | func (o *statefulSetLocalObject) GetServiceAccountName() string { 21 | return o.Object.(*appsv1.StatefulSet).Spec.Template.Spec.ServiceAccountName 22 | } 23 | 24 | func (o *statefulSetLocalObject) SetServiceAccountName(name string) { 25 | o.Object.(*appsv1.StatefulSet).Spec.Template.Spec.ServiceAccountName = name 26 | } 27 | 28 | func (o *statefulSetLocalObject) GetContainers() []corev1.Container { 29 | return o.Object.(*appsv1.StatefulSet).Spec.Template.Spec.Containers 30 | } 31 | 32 | func (o *statefulSetLocalObject) SetContainers(containers []corev1.Container) { 33 | o.Object.(*appsv1.StatefulSet).Spec.Template.Spec.Containers = containers 34 | } 35 | 36 | func (o *statefulSetLocalObject) GetInitContainers() []corev1.Container { 37 | return o.Object.(*appsv1.StatefulSet).Spec.Template.Spec.InitContainers 38 | } 39 | 40 | func (o *statefulSetLocalObject) SetInitContainers(containers []corev1.Container) { 41 | o.Object.(*appsv1.StatefulSet).Spec.Template.Spec.InitContainers = containers 42 | } 43 | 44 | func (o *statefulSetLocalObject) SetGVK() { 45 | o.Object.(*appsv1.StatefulSet).SetGroupVersionKind(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}) 46 | } 47 | 48 | func (o *statefulSetLocalObject) ResetStatus() { 49 | o.Object.(*appsv1.StatefulSet).Status = appsv1.StatefulSetStatus{} 50 | } 51 | 52 | func (o *statefulSetLocalObject) GetObject() client.Object { 53 | return o.Object 54 | } 55 | -------------------------------------------------------------------------------- /pkg/cmd/podidentity/root.go: -------------------------------------------------------------------------------- 1 | package podidentity 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | func NewPodIdentityCmd() *cobra.Command { 6 | podIdentityCmd := &cobra.Command{ 7 | Use: "podidentity", 8 | Short: "Configuration created for aad-pod-identity", 9 | Long: "Configuration created for aad-pod-identity", 10 | Aliases: []string{"pi"}, 11 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 12 | // run root command pre-run to register the debug flag 13 | if cmd.Root() != nil && cmd.Root().PersistentPreRunE != nil { 14 | if err := cmd.Root().PersistentPreRunE(cmd.Root(), args); err != nil { 15 | return err 16 | } 17 | } 18 | return nil 19 | }, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | return cmd.Usage() 22 | }, 23 | } 24 | 25 | podIdentityCmd.AddCommand(newDetectCmd()) 26 | 27 | return podIdentityCmd 28 | } 29 | -------------------------------------------------------------------------------- /pkg/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/spf13/cobra" 7 | _ "k8s.io/client-go/plugin/pkg/client/auth" // import auth plugins. See https://github.com/Azure/azure-workload-identity/issues/362. 8 | "monis.app/mlog" 9 | 10 | "github.com/Azure/azure-workload-identity/pkg/cmd/jwks" 11 | "github.com/Azure/azure-workload-identity/pkg/cmd/podidentity" 12 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount" 13 | "github.com/Azure/azure-workload-identity/pkg/cmd/version" 14 | ) 15 | 16 | const ( 17 | rootName = "azwi" 18 | rootShortDescription = "azwi helps to manage workload identity" 19 | rootLongDescription = rootShortDescription + " in Azure." 20 | ) 21 | 22 | var ( 23 | debug bool 24 | ) 25 | 26 | // NewRootCmd returns the root command for Azure Workload Identity. 27 | func NewRootCmd() *cobra.Command { 28 | flushLogs := mlog.Setup() 29 | cmd := &cobra.Command{ 30 | Use: rootName, 31 | Short: rootShortDescription, 32 | Long: rootLongDescription, 33 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 34 | // default to info instead of warning because existing info logs expect to always be printed 35 | logLevel := mlog.LevelInfo 36 | if debug { 37 | logLevel = mlog.LevelAll 38 | } 39 | 40 | // inputs are essentially static so this should never error 41 | return mlog.ValidateAndSetLogLevelAndFormatGlobally( 42 | context.Background(), // context is unused with mlog.FormatCLI 43 | mlog.LogSpec{ 44 | Level: logLevel, 45 | Format: mlog.FormatCLI, 46 | }, 47 | ) 48 | }, 49 | RunE: func(cmd *cobra.Command, args []string) error { 50 | return cmd.Usage() 51 | }, 52 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 53 | flushLogs() 54 | }, 55 | } 56 | 57 | p := cmd.PersistentFlags() 58 | p.BoolVar(&debug, "debug", false, "Enable debug logging") 59 | 60 | cmd.AddCommand(version.NewVersionCmd()) 61 | cmd.AddCommand(serviceaccount.NewServiceAccountCmd()) 62 | cmd.AddCommand(jwks.NewJWKSCmd()) 63 | cmd.AddCommand(podidentity.NewPodIdentityCmd()) 64 | 65 | return cmd 66 | } 67 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/options/errors.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // FlagIsRequiredError is returned when a required flag is not set 11 | func FlagIsRequiredError(name string) error { 12 | return errors.Errorf("--%s is required", name) 13 | } 14 | 15 | // OneOfFlagsIsRequiredError is returned when at least one of the flags is required 16 | func OneOfFlagsIsRequiredError(names ...string) error { 17 | flags := fmt.Sprintf("--%s", strings.Join(names, " or --")) 18 | return errors.Errorf("%s is required", flags) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/options/errors_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import "testing" 4 | 5 | func TestFlagIsRequiredError(t *testing.T) { 6 | err := FlagIsRequiredError("name") 7 | if err.Error() != "--name is required" { 8 | t.Errorf("FlagIsRequiredError() = %v, want %v", err, "--name is required") 9 | } 10 | } 11 | 12 | func TestOneOfFlagsIsRequiredError(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | flagNames []string 16 | errorMsg string 17 | }{ 18 | { 19 | name: "one flag", 20 | flagNames: []string{"name"}, 21 | errorMsg: "--name is required", 22 | }, 23 | { 24 | name: "two flags", 25 | flagNames: []string{"name", "namespace"}, 26 | errorMsg: "--name or --namespace is required", 27 | }, 28 | { 29 | name: "three flags", 30 | flagNames: []string{"name", "namespace", "cluster"}, 31 | errorMsg: "--name or --namespace or --cluster is required", 32 | }, 33 | } 34 | 35 | for _, test := range tests { 36 | t.Run(test.name, func(t *testing.T) { 37 | err := OneOfFlagsIsRequiredError(test.flagNames...) 38 | if err.Error() != test.errorMsg { 39 | t.Errorf("OneOfFlagsIsRequiredError() = %v, want %v", err, test.errorMsg) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/phases/create/data.go: -------------------------------------------------------------------------------- 1 | package phases 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/microsoftgraph/msgraph-sdk-go/models" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | 9 | "github.com/Azure/azure-workload-identity/pkg/cloud" 10 | ) 11 | 12 | // CreateData is the interface to use for create phase. 13 | // The "createData" type from cmd/create.go must satisfy this interface. 14 | type CreateData interface { 15 | // ServiceAccountName returns the name of the service account. 16 | ServiceAccountName() string 17 | 18 | // ServiceAccountNamespace returns the namespace of the service account. 19 | ServiceAccountNamespace() string 20 | 21 | // ServiceAccountIssuerURL returns the issuer URL of the service account. 22 | ServiceAccountIssuerURL() string 23 | 24 | // ServiceAccountTokenExpiration returns the expiration time of the service account token. 25 | ServiceAccountTokenExpiration() time.Duration 26 | 27 | // AADApplication returns the AAD application object. 28 | // This will return the cached value if it has been created. 29 | AADApplication() (models.Applicationable, error) 30 | 31 | // AADApplicationName returns the name of the AAD application. 32 | AADApplicationName() string 33 | 34 | // AADApplicationClientID returns the client ID of the AAD application. 35 | // This will be used for annotating the service account. 36 | AADApplicationClientID() string 37 | 38 | // AADApplicationObjectID returns the object ID of the AAD application. 39 | // This will be used for creating or removing the federated identity credential. 40 | AADApplicationObjectID() string 41 | 42 | // ServicePrincipal returns the service principal object. 43 | // This will return the cached value if it has been created. 44 | ServicePrincipal() (models.ServicePrincipalable, error) 45 | 46 | // ServicePrincipalName returns the name of the service principal. 47 | ServicePrincipalName() string 48 | 49 | // ServicePrincipalObjectID returns the object ID of the service principal. 50 | // This will be used for creating or removing the role assignment. 51 | ServicePrincipalObjectID() string 52 | 53 | // AzureRole returns the Azure role. 54 | AzureRole() string 55 | 56 | // AzureScope returns the Azure scope. 57 | AzureScope() string 58 | 59 | // AzureTenantID returns the Azure tenant ID. 60 | AzureTenantID() string 61 | 62 | // AzureClient returns the Azure client. 63 | AzureClient() cloud.Interface 64 | 65 | // KubeClient returns the Kubernetes client. 66 | KubeClient() (client.Client, error) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/phases/delete/aadapplication.go: -------------------------------------------------------------------------------- 1 | package phases 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | "monis.app/mlog" 8 | 9 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount/options" 10 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount/phases/workflow" 11 | ) 12 | 13 | const ( 14 | aadApplicationPhaseName = "aad-application" 15 | ) 16 | 17 | type aadApplicationPhase struct { 18 | } 19 | 20 | // NewAADApplicationPhase creates a new phase to delete an AAD application 21 | func NewAADApplicationPhase() workflow.Phase { 22 | p := &aadApplicationPhase{} 23 | return workflow.Phase{ 24 | Name: aadApplicationPhaseName, 25 | Aliases: []string{"app"}, 26 | Description: "Delete the Azure Active Directory (AAD) application and its underlying service principal", 27 | PreRun: p.prerun, 28 | Run: p.run, 29 | Flags: []string{ 30 | options.AADApplicationName.Flag, 31 | options.AADApplicationObjectID.Flag, 32 | }, 33 | } 34 | } 35 | 36 | func (p *aadApplicationPhase) prerun(data workflow.RunData) error { 37 | deleteData, ok := data.(DeleteData) 38 | if !ok { 39 | return errors.Errorf("invalid data type %T", data) 40 | } 41 | 42 | if deleteData.AADApplicationName() == "" && deleteData.AADApplicationObjectID() == "" { 43 | return options.OneOfFlagsIsRequiredError(options.AADApplicationName.Flag, options.AADApplicationObjectID.Flag) 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (p *aadApplicationPhase) run(ctx context.Context, data workflow.RunData) error { 50 | deleteData := data.(DeleteData) 51 | 52 | l := mlog.WithValues( 53 | "name", deleteData.AADApplicationName(), 54 | "objectID", deleteData.AADApplicationObjectID(), 55 | ).WithName(aadApplicationPhaseName) 56 | if err := deleteData.AzureClient().DeleteApplication(ctx, deleteData.AADApplicationObjectID()); err != nil { 57 | return errors.Wrap(err, "failed to delete application") 58 | } 59 | l.Info("deleted aad application") 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/phases/delete/aadapplication_test.go: -------------------------------------------------------------------------------- 1 | package phases 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/pkg/errors" 9 | 10 | "github.com/Azure/azure-workload-identity/pkg/cloud/mock_cloud" 11 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount/phases/workflow" 12 | ) 13 | 14 | func TestAADApplicationPreRun(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | phase workflow.Phase 18 | data interface{} 19 | errorMsg string 20 | }{ 21 | { 22 | name: "invalid data type", 23 | phase: NewAADApplicationPhase(), 24 | data: "test", 25 | errorMsg: "invalid data type string", 26 | }, 27 | { 28 | name: "valid data 1", 29 | phase: NewAADApplicationPhase(), 30 | data: &mockDeleteData{aadApplicationName: "test"}, 31 | errorMsg: "", 32 | }, 33 | { 34 | name: "valid data 2", 35 | phase: NewAADApplicationPhase(), 36 | data: &mockDeleteData{aadApplicationObjectID: "test"}, 37 | errorMsg: "", 38 | }, 39 | { 40 | name: "valid data 3", 41 | phase: NewAADApplicationPhase(), 42 | data: &mockDeleteData{serviceAccountNamespace: "test", serviceAccountName: "test", serviceAccountIssuerURL: "test"}, 43 | errorMsg: "", 44 | }, 45 | } 46 | 47 | for _, test := range tests { 48 | t.Run(test.name, func(t *testing.T) { 49 | err := test.phase.PreRun(test.data) 50 | if err == nil { 51 | if test.errorMsg != "" { 52 | t.Errorf("expected error but got nil") 53 | } 54 | } else if err.Error() != test.errorMsg { 55 | t.Errorf("expected error message: %s, but got: %s", test.errorMsg, err.Error()) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestAADApplicationRun(t *testing.T) { 62 | phase := NewAADApplicationPhase() 63 | data := &mockDeleteData{ 64 | aadApplicationObjectID: "aad-application-object-id", 65 | } 66 | 67 | ctrl := gomock.NewController(t) 68 | defer ctrl.Finish() 69 | 70 | mockAzureClient := mock_cloud.NewMockInterface(ctrl) 71 | mockAzureClient.EXPECT().DeleteApplication(gomock.Any(), data.aadApplicationObjectID).Return(nil) 72 | data.azureClient = mockAzureClient 73 | 74 | if err := phase.Run(context.Background(), data); err != nil { 75 | t.Errorf("expected no error but got: %s", err.Error()) 76 | } 77 | 78 | // Test for scenario where it failed to delete aad application 79 | mockAzureClient.EXPECT().DeleteApplication(gomock.Any(), data.aadApplicationObjectID).Return(errors.New("random error")) 80 | if err := phase.Run(context.Background(), data); err == nil { 81 | t.Errorf("expected error but got nil") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/phases/delete/data.go: -------------------------------------------------------------------------------- 1 | package phases 2 | 3 | import ( 4 | "github.com/microsoftgraph/msgraph-sdk-go/models" 5 | "sigs.k8s.io/controller-runtime/pkg/client" 6 | 7 | "github.com/Azure/azure-workload-identity/pkg/cloud" 8 | ) 9 | 10 | // DeleteData is the interface to use for create phase. 11 | // The "deleteData" type from cmd/delete.go must satisfy this interface. 12 | type DeleteData interface { 13 | // ServiceAccountName returns the name of the service account. 14 | ServiceAccountName() string 15 | 16 | // ServiceAccountNamespace returns the namespace of the service account. 17 | ServiceAccountNamespace() string 18 | 19 | // ServiceAccountIssuerURL returns the issuer URL of the service account. 20 | ServiceAccountIssuerURL() string 21 | 22 | // AADApplication returns the AAD application object. 23 | // This will return the cached value if it has been created. 24 | AADApplication() (models.Applicationable, error) 25 | 26 | // AADApplicationName returns the name of the AAD application. 27 | AADApplicationName() string 28 | 29 | // AADApplicationObjectID returns the object ID of the AAD application. 30 | // This will be used for creating or removing the federated identity credential. 31 | AADApplicationObjectID() string 32 | 33 | // RoleDefinitionID returns the role definition ID. 34 | RoleAssignmentID() string 35 | 36 | // AzureClient returns the Azure client. 37 | AzureClient() cloud.Interface 38 | 39 | // KubeClient returns the Kubernetes client. 40 | KubeClient() (client.Client, error) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/phases/delete/data_test.go: -------------------------------------------------------------------------------- 1 | package phases 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/microsoftgraph/msgraph-sdk-go/models" 7 | "github.com/pkg/errors" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | 10 | "github.com/Azure/azure-workload-identity/pkg/cloud" 11 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount/util" 12 | ) 13 | 14 | type mockDeleteData struct { 15 | serviceAccountName string 16 | serviceAccountNamespace string 17 | serviceAccountIssuerURL string 18 | aadApplication models.Applicationable // cache 19 | aadApplicationName string 20 | aadApplicationObjectID string 21 | roleAssignmentID string 22 | azureClient cloud.Interface 23 | kubeClient client.Client 24 | } 25 | 26 | var _ DeleteData = &mockDeleteData{} 27 | 28 | func (d *mockDeleteData) ServiceAccountName() string { 29 | return d.serviceAccountName 30 | } 31 | 32 | func (d *mockDeleteData) ServiceAccountNamespace() string { 33 | return d.serviceAccountNamespace 34 | } 35 | 36 | func (d *mockDeleteData) ServiceAccountIssuerURL() string { 37 | return d.serviceAccountIssuerURL 38 | } 39 | 40 | func (d *mockDeleteData) AADApplication() (models.Applicationable, error) { 41 | if d.aadApplication == nil { 42 | return nil, errors.New("not found") 43 | } 44 | return d.aadApplication, nil 45 | } 46 | 47 | func (d *mockDeleteData) AADApplicationName() string { 48 | if d.aadApplicationName == "" && d.ServiceAccountNamespace() != "" && d.ServiceAccountName() != "" && d.ServiceAccountIssuerURL() != "" { 49 | return fmt.Sprintf("%s-%s-%s", d.ServiceAccountNamespace(), d.serviceAccountName, util.GetIssuerHash(d.ServiceAccountIssuerURL())) 50 | } 51 | return d.aadApplicationName 52 | } 53 | 54 | func (d *mockDeleteData) AADApplicationObjectID() string { 55 | return d.aadApplicationObjectID 56 | } 57 | 58 | func (d *mockDeleteData) RoleAssignmentID() string { 59 | return d.roleAssignmentID 60 | } 61 | 62 | func (d *mockDeleteData) AzureClient() cloud.Interface { 63 | return d.azureClient 64 | } 65 | 66 | func (d *mockDeleteData) KubeClient() (client.Client, error) { 67 | if d.kubeClient == nil { 68 | return nil, errors.New("not found") 69 | } 70 | return d.kubeClient, nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/phases/delete/roleassignment.go: -------------------------------------------------------------------------------- 1 | package phases 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | "monis.app/mlog" 8 | 9 | "github.com/Azure/azure-workload-identity/pkg/cloud" 10 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount/options" 11 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount/phases/workflow" 12 | ) 13 | 14 | const ( 15 | roleAssignmentPhaseName = "role-assignment" 16 | ) 17 | 18 | type roleAssignmentPhase struct { 19 | } 20 | 21 | // NewRoleAssignmentPhase creates a new phase to delete role assignment 22 | func NewRoleAssignmentPhase() workflow.Phase { 23 | p := &roleAssignmentPhase{} 24 | return workflow.Phase{ 25 | Name: roleAssignmentPhaseName, 26 | Aliases: []string{"ra"}, 27 | Description: "Delete the role assignment between the AAD application and the Azure cloud resource", 28 | PreRun: p.prerun, 29 | Run: p.run, 30 | Flags: []string{options.RoleAssignmentID.Flag}, 31 | } 32 | } 33 | 34 | func (p *roleAssignmentPhase) prerun(data workflow.RunData) error { 35 | deleteData, ok := data.(DeleteData) 36 | if !ok { 37 | return errors.Errorf("invalid data type %T", data) 38 | } 39 | 40 | if deleteData.RoleAssignmentID() == "" { 41 | return options.FlagIsRequiredError(options.RoleAssignmentID.Flag) 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (p *roleAssignmentPhase) run(ctx context.Context, data workflow.RunData) error { 48 | deleteData := data.(DeleteData) 49 | 50 | // TODO(aramase): consider supporting deletion of role assignment with scope, role and application id 51 | // delete the role assignment 52 | l := mlog.WithValues( 53 | "roleAssignmentID", deleteData.RoleAssignmentID(), 54 | ).WithName(roleAssignmentPhaseName) 55 | if _, err := deleteData.AzureClient().DeleteRoleAssignment(ctx, deleteData.RoleAssignmentID()); err != nil { 56 | if !cloud.IsRoleAssignmentAlreadyDeleted(err) { 57 | return errors.Wrap(err, "failed to delete role assignment") 58 | } 59 | l.Warning("role assignment not found") 60 | } else { 61 | l.Info("deleted role assignment") 62 | } 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/phases/delete/serviceaccount.go: -------------------------------------------------------------------------------- 1 | package phases 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | apierrors "k8s.io/apimachinery/pkg/api/errors" 8 | "monis.app/mlog" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | 11 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount/options" 12 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount/phases/workflow" 13 | "github.com/Azure/azure-workload-identity/pkg/kuberneteshelper" 14 | ) 15 | 16 | const ( 17 | serviceAccountPhaseName = "service-account" 18 | ) 19 | 20 | type serviceAccountPhase struct { 21 | kubeClient client.Client 22 | } 23 | 24 | // NewServiceAccountPhase creates a new phase to delete the Kubernetes service account 25 | func NewServiceAccountPhase() workflow.Phase { 26 | p := &serviceAccountPhase{} 27 | return workflow.Phase{ 28 | Name: serviceAccountPhaseName, 29 | Aliases: []string{"sa"}, 30 | Description: "Delete the Kubernetes service account in the current KUBECONFIG context", 31 | PreRun: p.prerun, 32 | Run: p.run, 33 | Flags: []string{ 34 | options.ServiceAccountNamespace.Flag, 35 | options.ServiceAccountName.Flag, 36 | }, 37 | } 38 | } 39 | 40 | func (p *serviceAccountPhase) prerun(data workflow.RunData) error { 41 | deleteData, ok := data.(DeleteData) 42 | if !ok { 43 | return errors.Errorf("invalid data type %T", data) 44 | } 45 | 46 | if deleteData.ServiceAccountNamespace() == "" { 47 | return options.FlagIsRequiredError(options.ServiceAccountNamespace.Flag) 48 | } 49 | if deleteData.ServiceAccountName() == "" { 50 | return options.FlagIsRequiredError(options.ServiceAccountName.Flag) 51 | } 52 | 53 | var err error 54 | if p.kubeClient, err = deleteData.KubeClient(); err != nil { 55 | return errors.Wrap(err, "failed to get Kubernetes client") 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (p *serviceAccountPhase) run(ctx context.Context, data workflow.RunData) error { 62 | deleteData := data.(DeleteData) 63 | 64 | l := mlog.WithValues( 65 | "namespace", deleteData.ServiceAccountNamespace(), 66 | "name", deleteData.ServiceAccountName(), 67 | ).WithName(serviceAccountPhaseName) 68 | err := kuberneteshelper.DeleteServiceAccount( 69 | ctx, 70 | p.kubeClient, 71 | deleteData.ServiceAccountNamespace(), 72 | deleteData.ServiceAccountName(), 73 | ) 74 | if err != nil { 75 | if !apierrors.IsNotFound(err) { 76 | return errors.Wrap(err, "failed to delete service account") 77 | } 78 | l.Warning("service account not found") 79 | } else { 80 | l.Info("deleted service account") 81 | } 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/phases/workflow/phase.go: -------------------------------------------------------------------------------- 1 | package workflow 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Phase is a single phase of the workflow. 8 | type Phase struct { 9 | // Name is the name of the phase 10 | Name string 11 | 12 | // Aliases are alternative names for the phase 13 | Aliases []string 14 | 15 | // Description is the description of the phase 16 | Description string 17 | 18 | // PreRun is the function to run before the phase 19 | PreRun func(data RunData) error 20 | 21 | // Run is the function to run the phase 22 | Run func(ctx context.Context, data RunData) error 23 | 24 | // Flags is the list of flags to add to the command 25 | // when it is run as an individual phase 26 | Flags []string 27 | } 28 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/root.go: -------------------------------------------------------------------------------- 1 | package serviceaccount 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/Azure/azure-workload-identity/pkg/cmd/serviceaccount/auth" 7 | ) 8 | 9 | // NewServiceAccountCmd returns a new serviceaccount command 10 | func NewServiceAccountCmd() *cobra.Command { 11 | authProvider := auth.NewProvider() 12 | serviceAccountCmd := &cobra.Command{ 13 | Use: "serviceaccount", 14 | Short: "Manage the workload identity", 15 | Long: "Manage the workload identity", 16 | Aliases: []string{"sa"}, 17 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 18 | // run root command pre-run to register the debug flag 19 | if cmd.Root() != nil && cmd.Root().PersistentPreRunE != nil { 20 | if err := cmd.Root().PersistentPreRunE(cmd.Root(), args); err != nil { 21 | return err 22 | } 23 | } 24 | return authProvider.Validate() 25 | }, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | return cmd.Usage() 28 | }, 29 | } 30 | 31 | // auth flags should be available for all subcommands 32 | authProvider.AddFlags(serviceAccountCmd.PersistentFlags()) 33 | 34 | serviceAccountCmd.AddCommand(newCreateCmd(authProvider)) 35 | serviceAccountCmd.AddCommand(newDeleteCmd(authProvider)) 36 | 37 | return serviceAccountCmd 38 | } 39 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | "fmt" 7 | ) 8 | 9 | // GetIssuerHash returns a hash of the issuer URL 10 | func GetIssuerHash(issuerURL string) string { 11 | h := sha256.New() 12 | h.Write([]byte(issuerURL)) 13 | return base64.URLEncoding.EncodeToString(h.Sum(nil)) 14 | } 15 | 16 | // GetFederatedCredentialName returns a hash of 17 | // the service account namespace, name, and issuer URL 18 | func GetFederatedCredentialName(namespace, name, issuerURL string) string { 19 | h := sha256.New() 20 | h.Write([]byte(fmt.Sprintf("%s-%s-%s", namespace, name, issuerURL))) 21 | return base64.URLEncoding.EncodeToString(h.Sum(nil)) 22 | } 23 | 24 | // GetFederatedCredentialSubject returns the subject of the federated credential 25 | func GetFederatedCredentialSubject(namespace, name string) string { 26 | return fmt.Sprintf("system:serviceaccount:%s:%s", namespace, name) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/cmd/serviceaccount/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestGetIssuerHash(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | issuerURL string 9 | want string 10 | }{ 11 | { 12 | name: "empty", 13 | issuerURL: "", 14 | want: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU=", 15 | }, 16 | { 17 | name: "valid issuer", 18 | issuerURL: "https://test.blob.core.windows.net/oidc-test/", 19 | want: "foWt5lYFJx_-XwBetmnSltvWY5J_nenUV-2c3Lqes3o=", 20 | }, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | got := GetIssuerHash(tt.issuerURL) 26 | if got != tt.want { 27 | t.Errorf("GetIssuerHash() = %s, want %s", got, tt.want) 28 | } 29 | }) 30 | } 31 | } 32 | 33 | func TestGetFederatedCredentialName(t *testing.T) { 34 | tests := []struct { 35 | name string 36 | serviceAccountNamespace string 37 | serviceAccountName string 38 | issuerURL string 39 | want string 40 | }{ 41 | { 42 | name: "empty", 43 | serviceAccountNamespace: "", 44 | serviceAccountName: "", 45 | issuerURL: "", 46 | want: "2BVrrgxCQ9N0L8Tpd02KzqvgQQJJ1yDIVfmK_Ij_hGw=", 47 | }, 48 | { 49 | name: "valid", 50 | serviceAccountNamespace: "oidc", 51 | serviceAccountName: "pod-identity-sa", 52 | issuerURL: "https://test.blob.core.windows.net/oidc-test/", 53 | want: "5Frx_q5PpeP09cXWfkbDVwCOg5IVRmmKE3BUKT4hP4I=", 54 | }, 55 | } 56 | 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | got := GetFederatedCredentialName(tt.serviceAccountNamespace, tt.serviceAccountName, tt.issuerURL) 60 | if got != tt.want { 61 | t.Errorf("GetFederatedCredentialName() = %s, want %s", got, tt.want) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestGetFederatedCredentialSubject(t *testing.T) { 68 | want := "system:serviceaccount:oidc:pod-identity-sa" 69 | got := GetFederatedCredentialSubject("oidc", "pod-identity-sa") 70 | if got != want { 71 | t.Errorf("GetFederatedCredentialSubject() = %s, want %s", got, want) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pkg/cmd/version/root.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/Azure/azure-workload-identity/pkg/version" 9 | ) 10 | 11 | // NewVersionCmd returns a new version command 12 | func NewVersionCmd() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "version", 15 | Short: "Print the version of azwi", 16 | Long: "Print the version of azwi", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | fmt.Println(getVersion()) 19 | }, 20 | } 21 | 22 | return cmd 23 | } 24 | 25 | func getVersion() string { 26 | return fmt.Sprintf("Version: %s\nGitCommit: %s", version.BuildVersion, version.Vcs) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/cmd/version/root_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/Azure/azure-workload-identity/pkg/version" 7 | ) 8 | 9 | func TestGetVersion(t *testing.T) { 10 | version.BuildVersion = "v0.6.0" 11 | version.Vcs = "1ebf89c" 12 | 13 | expectedVersion := "Version: v0.6.0\nGitCommit: 1ebf89c" 14 | if getVersion() != expectedVersion { 15 | t.Errorf("getVersion() = %s, want %s", getVersion(), expectedVersion) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/kelseyhightower/envconfig" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // Config holds configuration from the env variables 9 | type Config struct { 10 | Cloud string `envconfig:"AZURE_ENVIRONMENT" default:"AzurePublicCloud"` 11 | TenantID string `envconfig:"AZURE_TENANT_ID" required:"true"` 12 | ProxyImage string `envconfig:"PROXY_IMAGE"` 13 | ProxyInitImage string `envconfig:"PROXY_INIT_IMAGE"` 14 | } 15 | 16 | // ParseConfig parses the configuration from env variables 17 | func ParseConfig() (*Config, error) { 18 | c := new(Config) 19 | if err := envconfig.Process("config", c); err != nil { 20 | return c, err 21 | } 22 | 23 | // validate parsed config 24 | if err := validateConfig(c); err != nil { 25 | return nil, err 26 | } 27 | return c, nil 28 | } 29 | 30 | // validateConfig validates the configuration 31 | func validateConfig(c *Config) error { 32 | if c.TenantID == "" { 33 | return errors.New("AZURE_TENANT_ID is required") 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestParseConfig(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | cloud string 12 | tenantID string 13 | wantErr bool 14 | wantCloud string 15 | }{ 16 | { 17 | name: "cloud name defaulting to AzurePublicCloud", 18 | cloud: "", 19 | tenantID: "tenant-id", 20 | wantCloud: "AzurePublicCloud", 21 | wantErr: false, 22 | }, 23 | { 24 | name: "cloud name set to AzureChinaCloud", 25 | cloud: "AzureChinaCloud", 26 | tenantID: "tenant-id", 27 | wantCloud: "AzureChinaCloud", 28 | wantErr: false, 29 | }, 30 | { 31 | name: "missing tenant id should return error", 32 | cloud: "AzureChinaCloud", 33 | tenantID: "", 34 | wantCloud: "", 35 | wantErr: true, 36 | }, 37 | } 38 | 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | os.Setenv("AZURE_TENANT_ID", tt.tenantID) 42 | os.Setenv("AZURE_ENVIRONMENT", tt.cloud) 43 | defer func() { 44 | os.Unsetenv("AZURE_TENANT_ID") 45 | os.Unsetenv("AZURE_ENVIRONMENT") 46 | }() 47 | 48 | c, err := ParseConfig() 49 | if (err != nil) != tt.wantErr { 50 | t.Fatalf("ParseConfig() error = %v, wantErr %v", err, tt.wantErr) 51 | } 52 | if !tt.wantErr { 53 | if c.Cloud != tt.cloud { 54 | t.Errorf("ParseConfig() got = %v, want %v", c.Cloud, tt.cloud) 55 | } 56 | if c.TenantID != tt.tenantID { 57 | t.Errorf("ParseConfig() got = %v, want %v", c.TenantID, tt.tenantID) 58 | } 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/kuberneteshelper/azureidentity.go: -------------------------------------------------------------------------------- 1 | package kuberneteshelper 2 | 3 | import ( 4 | "context" 5 | 6 | aadpodv1 "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | // ListAzureIdentity returns a list of AzureIdentity 11 | func ListAzureIdentity(ctx context.Context, kubeClient client.Client, namespace string) ([]aadpodv1.AzureIdentity, error) { 12 | list := &aadpodv1.AzureIdentityList{} 13 | if err := kubeClient.List(ctx, list, client.InNamespace(namespace)); err != nil { 14 | return nil, err 15 | } 16 | 17 | return list.Items, nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/kuberneteshelper/azureidentitybinding.go: -------------------------------------------------------------------------------- 1 | package kuberneteshelper 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | 7 | aadpodv1 "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1" 8 | "sigs.k8s.io/controller-runtime/pkg/client" 9 | ) 10 | 11 | type azureIdentityBindings []aadpodv1.AzureIdentityBinding 12 | 13 | func (a azureIdentityBindings) Len() int { 14 | return len(a) 15 | } 16 | 17 | func (a azureIdentityBindings) Swap(i, j int) { 18 | a[i], a[j] = a[j], a[i] 19 | } 20 | 21 | func (a azureIdentityBindings) Less(i, j int) bool { 22 | if a[i].Namespace == a[j].Namespace { 23 | return a[i].Name < a[j].Name 24 | } 25 | return a[i].Namespace < a[j].Namespace 26 | } 27 | 28 | // ListAzureIdentityBinding returns a list of AzureIdentityBinding 29 | func ListAzureIdentityBinding(ctx context.Context, kubeClient client.Client, namespace string) ([]aadpodv1.AzureIdentityBinding, error) { 30 | list := &aadpodv1.AzureIdentityBindingList{} 31 | if err := kubeClient.List(ctx, list, client.InNamespace(namespace)); err != nil { 32 | return nil, err 33 | } 34 | 35 | sort.Sort(azureIdentityBindings(list.Items)) 36 | return list.Items, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/kuberneteshelper/azureidentitybinding_test.go: -------------------------------------------------------------------------------- 1 | package kuberneteshelper 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | 8 | aadpodv1 "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1" 9 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | ) 11 | 12 | func TestSort(t *testing.T) { 13 | slice := []aadpodv1.AzureIdentityBinding{{ 14 | ObjectMeta: v1.ObjectMeta{ 15 | Name: "test2", 16 | Namespace: "test", 17 | }, 18 | }, { 19 | ObjectMeta: v1.ObjectMeta{ 20 | Name: "test1", 21 | Namespace: "default", 22 | }, 23 | }, { 24 | ObjectMeta: v1.ObjectMeta{ 25 | Name: "test3", 26 | Namespace: "default", 27 | }, 28 | }, { 29 | ObjectMeta: v1.ObjectMeta{ 30 | Name: "test1", 31 | Namespace: "test", 32 | }, 33 | }, { 34 | ObjectMeta: v1.ObjectMeta{ 35 | Name: "test2", 36 | Namespace: "default", 37 | }, 38 | }} 39 | expected := []aadpodv1.AzureIdentityBinding{{ 40 | ObjectMeta: v1.ObjectMeta{ 41 | Name: "test1", 42 | Namespace: "default", 43 | }, 44 | }, { 45 | ObjectMeta: v1.ObjectMeta{ 46 | Name: "test2", 47 | Namespace: "default", 48 | }, 49 | }, { 50 | ObjectMeta: v1.ObjectMeta{ 51 | Name: "test3", 52 | Namespace: "default", 53 | }, 54 | }, { 55 | ObjectMeta: v1.ObjectMeta{ 56 | Name: "test1", 57 | Namespace: "test", 58 | }, 59 | }, { 60 | ObjectMeta: v1.ObjectMeta{ 61 | Name: "test2", 62 | Namespace: "test", 63 | }, 64 | }} 65 | sort.Sort(azureIdentityBindings(slice)) 66 | if !reflect.DeepEqual(slice, expected) { 67 | t.Errorf("expected %v, got %v", expected, slice) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/kuberneteshelper/client.go: -------------------------------------------------------------------------------- 1 | package kuberneteshelper 2 | 3 | import ( 4 | "context" 5 | 6 | aadpodv1 "github.com/Azure/aad-pod-identity/pkg/apis/aadpodidentity/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/client-go/tools/clientcmd" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | var ( 17 | scheme = runtime.NewScheme() 18 | ) 19 | 20 | func init() { 21 | _ = clientgoscheme.AddToScheme(scheme) 22 | 23 | // Add aadpodidentity v1 to the scheme. 24 | aadPodIdentityGroupVersion := schema.GroupVersion{Group: aadpodv1.GroupName, Version: "v1"} 25 | scheme.AddKnownTypes(aadPodIdentityGroupVersion, 26 | &aadpodv1.AzureIdentity{}, 27 | &aadpodv1.AzureIdentityList{}, 28 | &aadpodv1.AzureIdentityBinding{}, 29 | &aadpodv1.AzureIdentityBindingList{}, 30 | ) 31 | metav1.AddToGroupVersion(scheme, aadPodIdentityGroupVersion) 32 | } 33 | 34 | // GetKubeConfig returns the kubeconfig 35 | func GetKubeConfig() (*rest.Config, error) { 36 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}).ClientConfig() 37 | } 38 | 39 | // GetKubeClient returns a Kubernetes clientset. 40 | func GetKubeClient() (client.Client, error) { 41 | kubeConfig, err := GetKubeConfig() 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return client.New(kubeConfig, client.Options{Scheme: scheme}) 47 | } 48 | 49 | // GetObject returns an object from the Kubernetes cluster. 50 | func GetObject(ctx context.Context, kubeClient client.Client, namespace string, name string, obj client.Object) (client.Object, error) { 51 | err := kubeClient.Get(ctx, client.ObjectKey{ 52 | Namespace: namespace, 53 | Name: name, 54 | }, obj) 55 | 56 | return obj, err 57 | } 58 | -------------------------------------------------------------------------------- /pkg/kuberneteshelper/pod.go: -------------------------------------------------------------------------------- 1 | package kuberneteshelper 2 | 3 | import ( 4 | "context" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | // ListPods returns a list of Pods in the given namespace that match the given label selector 11 | func ListPods(ctx context.Context, kubeClient client.Client, namespace string, labels map[string]string) (map[string]corev1.Pod, error) { 12 | list := &corev1.PodList{} 13 | if err := kubeClient.List(ctx, list, client.InNamespace(namespace), client.MatchingLabels(labels)); err != nil { 14 | return nil, err 15 | } 16 | 17 | podMap := make(map[string]corev1.Pod) 18 | for _, pod := range list.Items { 19 | podMap[pod.Name] = pod 20 | } 21 | 22 | return podMap, nil 23 | } 24 | -------------------------------------------------------------------------------- /pkg/kuberneteshelper/serviceaccount.go: -------------------------------------------------------------------------------- 1 | package kuberneteshelper 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | corev1 "k8s.io/api/core/v1" 9 | apierrors "k8s.io/apimachinery/pkg/api/errors" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | 13 | "github.com/Azure/azure-workload-identity/pkg/webhook" 14 | ) 15 | 16 | // Create ServiceAccount in the cluster 17 | // If the ServiceAccount already exists, error is returned 18 | func CreateOrUpdateServiceAccount(ctx context.Context, kubeClient client.Client, namespace, name, clientID, tenantID string, tokenExpiration time.Duration) error { 19 | sa := &corev1.ServiceAccount{ 20 | ObjectMeta: metav1.ObjectMeta{ 21 | Name: name, 22 | Namespace: namespace, 23 | Annotations: map[string]string{ 24 | webhook.ClientIDAnnotation: clientID, 25 | webhook.TenantIDAnnotation: tenantID, 26 | }, 27 | }, 28 | } 29 | 30 | if tokenExpiration != time.Duration(webhook.DefaultServiceAccountTokenExpiration)*time.Second { 31 | // Round to the nearest second before converting to a string 32 | sa.ObjectMeta.Annotations[webhook.ServiceAccountTokenExpiryAnnotation] = fmt.Sprintf("%.0f", tokenExpiration.Round(time.Second).Seconds()) 33 | } 34 | 35 | err := kubeClient.Create(ctx, sa) 36 | if apierrors.IsAlreadyExists(err) { 37 | err = kubeClient.Update(ctx, sa) 38 | } 39 | return err 40 | } 41 | 42 | // Delete ServiceAccount in the cluster 43 | func DeleteServiceAccount(ctx context.Context, kubeClient client.Client, namespace, name string) error { 44 | sa := &corev1.ServiceAccount{} 45 | if err := kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, sa); err != nil { 46 | return err 47 | } 48 | return kubeClient.Delete(ctx, sa) 49 | } 50 | 51 | // Get ServiceAccount in the cluster 52 | func GetServiceAccount(ctx context.Context, kubeClient client.Client, namespace, name string) (*corev1.ServiceAccount, error) { 53 | sa := &corev1.ServiceAccount{} 54 | err := kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, sa) 55 | return sa, err 56 | } 57 | -------------------------------------------------------------------------------- /pkg/metrics/exporter.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/Azure/azure-workload-identity/pkg/metrics/exporters/prometheus" 8 | ) 9 | 10 | func InitMetricsExporter(metricsBackend string) error { 11 | mb := strings.ToLower(metricsBackend) 12 | switch mb { 13 | // Prometheus is the only exporter for now 14 | case prometheus.ExporterName: 15 | return prometheus.InitExporter() 16 | default: 17 | return fmt.Errorf("unsupported metrics backend: %v", metricsBackend) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/metrics/exporter_test.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "testing" 4 | 5 | func TestInitMetricsExporter(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | metricsBackend string 9 | }{ 10 | { 11 | name: "prometheus", 12 | metricsBackend: "prometheus", 13 | }, 14 | { 15 | name: "Prometheus", 16 | metricsBackend: "Prometheus", 17 | }, 18 | } 19 | 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | if err := InitMetricsExporter(tt.metricsBackend); err != nil { 23 | t.Errorf("InitMetricsExporter() error = %v, expected nil", err) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | func TestInitMetricsExporterError(t *testing.T) { 30 | if err := InitMetricsExporter("unknown"); err == nil { 31 | t.Errorf("InitMetricsExporter() error = nil, expected error") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/metrics/exporters/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | crprometheus "github.com/prometheus/client_golang/prometheus" 5 | "go.opentelemetry.io/otel" 6 | "go.opentelemetry.io/otel/exporters/prometheus" 7 | "go.opentelemetry.io/otel/sdk/metric" 8 | "sigs.k8s.io/controller-runtime/pkg/metrics" 9 | ) 10 | 11 | const ( 12 | // ExporterName is the name of the exporter 13 | ExporterName = "prometheus" 14 | ) 15 | 16 | func InitExporter() error { 17 | exporter, err := prometheus.New( 18 | prometheus.WithRegisterer(metrics.Registry.(*crprometheus.Registry)), 19 | ) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | meterProvider := metric.NewMeterProvider( 25 | metric.WithReader(exporter), 26 | metric.WithView(metric.NewView( 27 | metric.Instrument{Name: "azwi_*"}, 28 | metric.Stream{ 29 | Aggregation: metric.AggregationExplicitBucketHistogram{ 30 | Boundaries: []float64{0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.5, 2, 2.5, 3}, 31 | }}, 32 | )), 33 | ) 34 | 35 | otel.SetMeterProvider(meterProvider) 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/proxy/probe.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | const ( 12 | // retryCount is the number of times to retry probing the proxy. 13 | retryCount = 7 14 | // waitTime is the time to wait between retries. 15 | waitTime = time.Second 16 | // clientTimeout is the timeout for the client. 17 | clientTimeout = time.Second * 5 18 | ) 19 | 20 | // Probe checks if the proxy is ready to serve requests. 21 | func Probe(port int) error { 22 | url := fmt.Sprintf("http://%s:%d%s", localhost, port, readyzPathPrefix) 23 | return probe(url) 24 | } 25 | 26 | func probe(url string) error { 27 | client := &http.Client{ 28 | Timeout: clientTimeout, 29 | } 30 | for i := 0; i < retryCount; i++ { 31 | req, err := http.NewRequest(http.MethodGet, url, nil) 32 | if err != nil { 33 | return err 34 | } 35 | resp, err := client.Do(req) 36 | if err != nil { 37 | return err 38 | } 39 | if resp.StatusCode == http.StatusOK { 40 | return nil 41 | } 42 | time.Sleep(waitTime) 43 | } 44 | return errors.Errorf("failed to probe proxy") 45 | } 46 | -------------------------------------------------------------------------------- /pkg/proxy/probe_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "testing" 5 | 6 | "monis.app/mlog" 7 | ) 8 | 9 | func TestProbe(t *testing.T) { 10 | setup() 11 | defer teardown() 12 | 13 | p := &proxy{logger: mlog.New()} 14 | rtr.PathPrefix("/readyz").HandlerFunc(p.readyzHandler) 15 | 16 | if err := probe(server.URL + "/readyz"); err != nil { 17 | t.Errorf("probe() = %v, want nil", err) 18 | } 19 | } 20 | 21 | func TestProbeError(t *testing.T) { 22 | setup() 23 | defer teardown() 24 | 25 | if err := probe(server.URL + "/readyz"); err == nil { 26 | t.Errorf("probe() = nil, want error") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/util/pod_info.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "os" 4 | 5 | // GetNamespace returns the namespace for azure-wi-webhook 6 | func GetNamespace() string { 7 | ns, found := os.LookupEnv("POD_NAMESPACE") 8 | if !found { 9 | return "azure-workload-identity-system" 10 | } 11 | return ns 12 | } 13 | -------------------------------------------------------------------------------- /pkg/util/pod_info_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestGetNamespace(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | podNamespace string 12 | want string 13 | }{ 14 | { 15 | name: "default webhook namespace", 16 | podNamespace: "", 17 | want: "azure-workload-identity-system", 18 | }, 19 | { 20 | name: "namespace set", 21 | podNamespace: "kube-system", 22 | want: "kube-system", 23 | }, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | if tt.podNamespace != "" { 28 | os.Setenv("POD_NAMESPACE", tt.podNamespace) 29 | defer os.Unsetenv("POD_NAMESPACE") 30 | } 31 | 32 | if got := GetNamespace(); got != tt.want { 33 | t.Errorf("GetNamespace() = %v, want %v", got, tt.want) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | // Vcs is the commit hash for the binary build 10 | Vcs string 11 | // BuildTime is the date for the binary build 12 | BuildTime string 13 | // BuildVersion is the azure-workload-identity version. Will be overwritten from build. 14 | BuildVersion string 15 | ) 16 | 17 | // GetUserAgent returns a user agent of the format: azure-workload-identity/ (/) / 18 | func GetUserAgent(component string) string { 19 | return fmt.Sprintf("azure-workload-identity/%s/%s (%s/%s) %s/%s", component, BuildVersion, runtime.GOOS, runtime.GOARCH, Vcs, BuildTime) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestGetUserAgent(t *testing.T) { 11 | BuildTime = "Now" 12 | BuildVersion = "version" 13 | Vcs = "hash" 14 | 15 | expected := fmt.Sprintf("azure-workload-identity/webhook/%s (%s/%s) %s/%s", BuildVersion, runtime.GOOS, runtime.GOARCH, Vcs, BuildTime) 16 | actual := GetUserAgent("webhook") 17 | if !strings.EqualFold(expected, actual) { 18 | t.Fatalf("expected: %s, got: %s", expected, actual) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/webhook/stats_reporter.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "go.opentelemetry.io/otel" 8 | "go.opentelemetry.io/otel/attribute" 9 | "go.opentelemetry.io/otel/metric" 10 | ) 11 | 12 | const ( 13 | requestDurationMetricName = "azwi_mutation_request" 14 | 15 | namespaceKey = "namespace" 16 | ) 17 | 18 | var ( 19 | req metric.Float64Histogram 20 | // if service.name is not specified, the default is "unknown_service:" 21 | // xref: https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/#service 22 | labels = []attribute.KeyValue{attribute.String("service.name", "webhook")} 23 | ) 24 | 25 | func registerMetrics() error { 26 | var err error 27 | meter := otel.Meter("webhook") 28 | 29 | req, err = meter.Float64Histogram( 30 | requestDurationMetricName, 31 | metric.WithDescription("Distribution of how long it took for the azure-workload-identity mutation request")) 32 | 33 | return err 34 | } 35 | 36 | // ReportRequest reports the request duration for the given namespace. 37 | func ReportRequest(ctx context.Context, namespace string, duration time.Duration) { 38 | l := append(labels, attribute.String(namespaceKey, namespace)) 39 | req.Record(ctx, duration.Seconds(), metric.WithAttributes(l...)) 40 | } 41 | -------------------------------------------------------------------------------- /scripts/create-aks-cluster.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | : "${CLUSTER_NAME:?Environment variable empty or not defined.}" 8 | 9 | get_random_region() { 10 | local REGIONS=("eastus" "eastus2" "southcentralus" "westeurope" "uksouth" "northeurope" "francecentral") 11 | echo "${REGIONS[${RANDOM} % ${#REGIONS[@]}]}" 12 | } 13 | 14 | should_create_aks_cluster() { 15 | if [[ "${SOAK_CLUSTER:-}" == "true" ]] || [[ -n "${KUBECONFIG:-}" ]]; then 16 | echo "false" && return 17 | fi 18 | if az aks show --resource-group "${CLUSTER_NAME}" --name "${CLUSTER_NAME}" > /dev/null; then 19 | echo "false" && return 20 | fi 21 | echo "true" && return 22 | } 23 | 24 | main() { 25 | if [[ "$(should_create_aks_cluster)" == "true" ]]; then 26 | echo "Creating an AKS cluster '${CLUSTER_NAME}'" 27 | LOCATION="$(get_random_region)" 28 | az group create --name "${CLUSTER_NAME}" --location "${LOCATION}" > /dev/null 29 | # TODO(chewong): ability to create an arc-enabled cluster 30 | az aks create \ 31 | --resource-group "${CLUSTER_NAME}" \ 32 | --name "${CLUSTER_NAME}" \ 33 | --node-vm-size Standard_DS3_v2 \ 34 | --enable-managed-identity \ 35 | --network-plugin azure \ 36 | --node-count 3 \ 37 | --generate-ssh-keys \ 38 | --enable-oidc-issuer > /dev/null 39 | if [[ "${WINDOWS_CLUSTER:-}" == "true" ]]; then 40 | # shellcheck disable=SC2086 41 | az aks nodepool add --resource-group "${CLUSTER_NAME}" --cluster-name "${CLUSTER_NAME}" --os-type Windows --name npwin --node-count 3 ${EXTRA_ARGS:-} > /dev/null 42 | fi 43 | fi 44 | } 45 | 46 | main 47 | -------------------------------------------------------------------------------- /test/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | package e2e 4 | 5 | import ( 6 | "flag" 7 | "os" 8 | "testing" 9 | 10 | "k8s.io/kubernetes/test/e2e/framework" 11 | "k8s.io/kubernetes/test/e2e/framework/config" 12 | ) 13 | 14 | func init() { 15 | flag.StringVar(&tokenExchangeE2EImage, "e2e.token-exchange-image", "aramase/msal-go:v0.6.0", "The image to use for token exchange tests") 16 | } 17 | 18 | // handleFlags sets up all flags and parses the command line. 19 | func handleFlags() { 20 | config.CopyFlags(config.Flags, flag.CommandLine) 21 | framework.RegisterCommonFlags(flag.CommandLine) 22 | framework.RegisterClusterFlags(flag.CommandLine) 23 | flag.Parse() 24 | } 25 | 26 | func TestMain(m *testing.M) { 27 | // Register test flags, then parse flags. 28 | handleFlags() 29 | framework.AfterReadingAllFlags(&framework.TestContext) 30 | 31 | os.Exit(m.Run()) 32 | } 33 | 34 | func TestE2E(t *testing.T) { 35 | RunE2ETests(t) 36 | } 37 | -------------------------------------------------------------------------------- /third_party/open-policy-agent/gatekeeper/helmify/README.md: -------------------------------------------------------------------------------- 1 | # gatekeeper/helmify 2 | 3 | Forked from https://github.com/open-policy-agent/gatekeeper (v3.5.0-rc.1). 4 | 5 | The helmify helps auto-generate the helm chart from manifest to avoid any drifts 6 | 7 | The original code can be found at https://github.com/open-policy-agent/gatekeeper/tree/master/cmd/build/helmify. 8 | -------------------------------------------------------------------------------- /third_party/open-policy-agent/gatekeeper/helmify/delete-ports.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: azure-wi-webhook-controller-manager 5 | namespace: azure-workload-identity-system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 8095 13 | name: metrics 14 | protocol: TCP 15 | $patch: delete 16 | -------------------------------------------------------------------------------- /third_party/open-policy-agent/gatekeeper/helmify/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: "{{ .Release.Namespace }}" 2 | commonLabels: 3 | app: '{{ template "workload-identity-webhook.name" . }}' 4 | chart: '{{ template "workload-identity-webhook.name" . }}' 5 | release: "{{ .Release.Name }}" 6 | bases: 7 | - "../../../../config/default" # calls ../../default 8 | patchesStrategicMerge: 9 | - kustomize-for-helm.yaml 10 | - delete-ports.yaml 11 | patchesJson6902: 12 | # these are defined in the chart values rather than hard-coded 13 | - target: 14 | kind: Deployment 15 | name: azure-wi-webhook-controller-manager 16 | patch: |- 17 | - op: remove 18 | path: /spec/template/spec/containers/0/resources/limits 19 | - op: remove 20 | path: /spec/template/spec/containers/0/resources/requests 21 | - op: remove 22 | path: /spec/template/spec/nodeSelector/kubernetes.io~1os 23 | -------------------------------------------------------------------------------- /third_party/open-policy-agent/gatekeeper/helmify/replacements.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var replacements = map[string]string{ 4 | `HELMSUBST_DEPLOYMENT_CONTAINER_RESOURCES: ""`: `{{- toYaml .Values.resources | nindent 10 }}`, 5 | 6 | `HELMSUBST_DEPLOYMENT_NODE_SELECTOR: ""`: `{{- toYaml .Values.nodeSelector | nindent 8 }}`, 7 | 8 | "HELMSUBST_DEPLOYMENT_REPLICAS": `{{ .Values.replicaCount }}`, 9 | 10 | `HELMSUBST_DEPLOYMENT_AFFINITY: ""`: `{{- toYaml .Values.affinity | nindent 8 }}`, 11 | 12 | `HELMSUBST_DEPLOYMENT_TOLERATIONS: ""`: `{{- toYaml .Values.tolerations | nindent 8 }}`, 13 | 14 | "HELMSUBST_CONFIGMAP_AZURE_ENVIRONMENT": `{{ .Values.azureEnvironment | default "AzurePublicCloud" }}`, 15 | 16 | "HELMSUBST_CONFIGMAP_AZURE_TENANT_ID": `{{ required "A valid .Values.azureTenantID entry required!" .Values.azureTenantID }}`, 17 | 18 | `HELMSUBST_SERVICE_TYPE: ""`: `{{- if .Values.service }} 19 | type: {{ .Values.service.type | default "ClusterIP" }} 20 | {{- end }}`, 21 | 22 | "HELMSUBST_DEPLOYMENT_METRICS_PORT": `{{ trimPrefix ":" .Values.metricsAddr }}`, 23 | 24 | "HELMSUBST_DEPLOYMENT_PRIORITY_CLASS_NAME": `{{ .Values.priorityClassName }}`, 25 | 26 | `HELMSUBST_MUTATING_WEBHOOK_ANNOTATIONS: ""`: `{{- toYaml .Values.mutatingWebhookAnnotations | nindent 4 }}`, 27 | 28 | `HELMSUBST_SERVICEACCOUNT_IMAGE_PULL_SECRETS: ""`: `{{- if .Values.imagePullSecrets }} 29 | imagePullSecrets: 30 | {{- toYaml .Values.imagePullSecrets | nindent 2 }} 31 | {{- end }}`, 32 | 33 | `HELMSUBST_MUTATING_WEBHOOK_NAMESPACE_SELECTOR`: `{{- toYaml .Values.mutatingWebhookNamespaceSelector | nindent 4 }}`, 34 | 35 | `HELMSUBST_POD_ANNOTATIONS: ""`: `{{- toYaml .Values.podAnnotations | trim | nindent 8 }}`, 36 | 37 | `minAvailable: HELMSUBST_PODDISRUPTIONBUDGET_MINAVAILABLE`: `{{- if .Values.podDisruptionBudget.minAvailable }} 38 | minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} 39 | {{- end }}`, 40 | 41 | `HELMSUBST_PODDISRUPTIONBUDGET_MAXUNAVAILABLE: ""`: `{{- if .Values.podDisruptionBudget.maxUnavailable }} 42 | maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} 43 | {{- end }}`, 44 | } 45 | -------------------------------------------------------------------------------- /third_party/open-policy-agent/gatekeeper/helmify/static/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /third_party/open-policy-agent/gatekeeper/helmify/static/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: workload-identity-webhook 3 | description: A Helm chart to install the azure-workload-identity webhook 4 | type: application 5 | version: 1.5.0 6 | appVersion: v1.5.0 7 | home: https://github.com/Azure/azure-workload-identity 8 | sources: 9 | - https://github.com/Azure/azure-workload-identity 10 | -------------------------------------------------------------------------------- /third_party/open-policy-agent/gatekeeper/helmify/static/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "workload-identity-webhook.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "workload-identity-webhook.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "workload-identity-webhook.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "workload-identity-webhook.labels" -}} 37 | helm.sh/chart: {{ include "workload-identity-webhook.chart" . }} 38 | {{ include "workload-identity-webhook.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "workload-identity-webhook.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "workload-identity-webhook.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Adds the pod labels. 55 | */}} 56 | {{- define "workload-identity-webhook.podLabels" -}} 57 | {{- if .Values.podLabels }} 58 | {{- toYaml .Values.podLabels | nindent 8 }} 59 | {{- end }} 60 | {{- end }} 61 | -------------------------------------------------------------------------------- /third_party/open-policy-agent/gatekeeper/helmify/static/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for workload-identity-webhook. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 2 6 | image: 7 | repository: mcr.microsoft.com/oss/azure/workload-identity/webhook 8 | pullPolicy: IfNotPresent 9 | # Overrides the image tag whose default is the chart appVersion. 10 | release: v1.5.0 11 | imagePullSecrets: [] 12 | nodeSelector: 13 | kubernetes.io/os: linux 14 | resources: 15 | limits: 16 | cpu: 100m 17 | memory: 30Mi 18 | requests: 19 | cpu: 100m 20 | memory: 20Mi 21 | tolerations: [] 22 | affinity: {} 23 | service: 24 | type: ClusterIP 25 | port: 443 26 | targetPort: 9443 27 | azureEnvironment: AzurePublicCloud 28 | azureTenantID: 29 | logLevel: info 30 | metricsAddr: ":8095" 31 | metricsBackend: prometheus 32 | priorityClassName: system-cluster-critical 33 | mutatingWebhookAnnotations: {} 34 | podLabels: {} 35 | podAnnotations: {} 36 | mutatingWebhookNamespaceSelector: {} 37 | # minAvailable and maxUnavailable are mutually exclusive 38 | podDisruptionBudget: 39 | minAvailable: 1 40 | # maxUnavailable: 0 41 | --------------------------------------------------------------------------------