From a9f51c4f8281aabc3fad6f09586d77878e81e7c0 Mon Sep 17 00:00:00 2001 From: n-marton Date: Wed, 18 Dec 2024 12:45:49 +0100 Subject: [PATCH] initial code Signed-off-by: n-marton --- .github/CODEOWNERS | 1 + .github/dependabot.yml | 7 + .github/workflows/build_latest_image.yml | 39 ++++ .github/workflows/stale_issues_bot.yml | 18 ++ .github/workflows/truffle_hog.yml | 30 +++ DESIGN.md | 7 + Dockerfile | 19 ++ GUIDE.md | 11 + LOGO.png | Bin 0 -> 16570 bytes README.md | 25 +++ STARTED.md | 19 ++ diagram.png | Bin 0 -> 100139 bytes go.mod | 91 ++++++++ go.sum | 262 +++++++++++++++++++++++ main.go | 7 + manifests/apiservice.yaml | 14 ++ manifests/deployment.yaml | 46 ++++ manifests/kustomization.yaml | 11 + manifests/rbac.yaml | 30 +++ manifests/secrets.yaml | 9 + manifests/service.yaml | 15 ++ manifests/webhook.yaml | 21 ++ plugin/plugin.go | 206 ++++++++++++++++++ renovate.json | 10 + rexec/main.go | 26 +++ rexec/server/async.go | 52 +++++ rexec/server/config.go | 96 +++++++++ rexec/server/parser.go | 68 ++++++ rexec/server/server.go | 258 ++++++++++++++++++++++ rexec/server/tcp.go | 133 ++++++++++++ 30 files changed, 1531 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build_latest_image.yml create mode 100644 .github/workflows/stale_issues_bot.yml create mode 100644 .github/workflows/truffle_hog.yml create mode 100644 DESIGN.md create mode 100644 Dockerfile create mode 100644 GUIDE.md create mode 100644 LOGO.png create mode 100644 README.md create mode 100644 STARTED.md create mode 100644 diagram.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 manifests/apiservice.yaml create mode 100644 manifests/deployment.yaml create mode 100644 manifests/kustomization.yaml create mode 100644 manifests/rbac.yaml create mode 100644 manifests/secrets.yaml create mode 100644 manifests/service.yaml create mode 100644 manifests/webhook.yaml create mode 100644 plugin/plugin.go create mode 100644 renovate.json create mode 100644 rexec/main.go create mode 100644 rexec/server/async.go create mode 100644 rexec/server/config.go create mode 100644 rexec/server/parser.go create mode 100644 rexec/server/server.go create mode 100644 rexec/server/tcp.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ab872d1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Adyen/container-services diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c684890 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +--- +version: 2 +updates: +- package-ecosystem: "gomod" + directory: "/rexec" + schedule: + interval: "weekly" diff --git a/.github/workflows/build_latest_image.yml b/.github/workflows/build_latest_image.yml new file mode 100644 index 0000000..b246232 --- /dev/null +++ b/.github/workflows/build_latest_image.yml @@ -0,0 +1,39 @@ +name: build rexec proxy image +on: + push: + branches: + - main + tags: + - 'v*' +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: 'Login to GitHub Container Registry' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{github.actor}} + password: ${{secrets.GITHUB_TOKEN}} + - name: Log into registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push latest + uses: docker/build-push-action@v6 + with: + push: true + tags: ghcr.io/adyen/kubectl-rexec:latest + - name: Build and push ref + uses: docker/build-push-action@v6 + with: + push: true + tags: ghcr.io/adyen/kubectl-rexec:${{github.ref_name}} diff --git a/.github/workflows/stale_issues_bot.yml b/.github/workflows/stale_issues_bot.yml new file mode 100644 index 0000000..cc1516e --- /dev/null +++ b/.github/workflows/stale_issues_bot.yml @@ -0,0 +1,18 @@ +name: Github Stale Issues Check + +on: + schedule: + - cron: '59 23 * * *' # Run every day just before midnight + +jobs: + close_stale_prs: + runs-on: ubuntu-latest + steps: + - name: Close stale issues + uses: actions/stale@v9 + with: + any-of-labels: 'Needs more info' + stale-issue-message: 'This issue is stale because it has been open 21 days with no activity. Please comment on this issue otherwise it will be closed in 7 days.' + close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' + days-before-issue-stale: 21 + days-before-issue-close: 7 diff --git a/.github/workflows/truffle_hog.yml b/.github/workflows/truffle_hog.yml new file mode 100644 index 0000000..34fa583 --- /dev/null +++ b/.github/workflows/truffle_hog.yml @@ -0,0 +1,30 @@ +on: + push: + branches: + - main + pull_request: + +jobs: + TruffleHog: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: TruffleHog OSS + id: trufflehog + uses: trufflesecurity/trufflehog@v3.87.0 + continue-on-error: true + with: + path: ./ + base: "${{ github.event.repository.default_branch }}" + head: HEAD + + - name: Scan Results Status + if: steps.trufflehog.outcome == 'failure' + run: exit 1 \ No newline at end of file diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..2acbb0f --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,7 @@ +## How does rexec work? + +The setup consists of two parts, first we have a `ValidatingWebhookConfiguration` where, we deny requests targeting pod exec unless the user is allowed to bypass or the request is coming through the rexec endpont. + +The second part is the rexec `APIService` where we receive exec request with the custom plugin. Here we modify the request back to a normal exec and audit it while proxying back to the kube apiserver. This proxyiing is happening through impersonation, as the user credentials are removed by the kube apiserver before being proxied to here. + +![Diagram](diagram.png?raw=true "Diagram") \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dac3b04 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.23-bookworm AS builder + +LABEL org.opencontainers.image.source=https://github.com/adyen/kubectl-rexec +LABEL org.opencontainers.image.description="Rexec proxy" +LABEL org.opencontainers.image.licenses=MIT + +WORKDIR /workspace +COPY go.mod go.mod +COPY go.sum go.sum +COPY rexec/main.go main.go +COPY rexec/server rexec/server + +RUN CGO_ENABLED=0 go build -a -o rexec-server . + +FROM scratch +WORKDIR / +COPY --from=builder /workspace/rexec-server . + +ENTRYPOINT ["/rexec-server"] diff --git a/GUIDE.md b/GUIDE.md new file mode 100644 index 0000000..65fe37b --- /dev/null +++ b/GUIDE.md @@ -0,0 +1,11 @@ +## Configuration + +`--sys-debug` if set the api will log more verbose information about internal events + +`--audit-trace` if set, and tty was requested all keystrokes will be logged (otherwise the async auditer will merge keystrokes into command on each new lines) + +`--by-pass-user` repeatable flag for adding users to bypass list so they can use the standard exec command, handy for system users like `system:admin` + +`--by-pass-shared-key` this flags needs to be set if one runes more then one replica of rexec api, so the shared key between the apiservice part and the validatingwebhookpart are matching, otherwise said hey is autogenerated, it has to be a RFC 4122 compliant uuid + +`--max-strokes-per-line` with this flag we can alter the treshold we have on a linelength before async audit flushes, keep in mind the increasing it too high might lead oom kills on the rexec server \ No newline at end of file diff --git a/LOGO.png b/LOGO.png new file mode 100644 index 0000000000000000000000000000000000000000..e390c5b185a6b0393671659be068dd6f82245432 GIT binary patch literal 16570 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..69bff14 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# kubectl-rexec +![LOGO](LOGO.png) + +Kubectl exec does not provide any kind of audit what is actually done inside the container. Rexec plugin is here to help with that. + +## Contributing +We strongly encourage you to contribute to our repository. Find out more in our [contribution guidelines](https://github.com/Adyen/.github/blob/master/CONTRIBUTING.md) + +## Requirements +In kubernetes 1.30 `TranslateStreamCloseWebsocketRequests` featuregate is true by the default making protocol between kubectl and kube-apiserver is websocket while prior is SPDY, this solution handles only websockets so the k8s cluster either has to be 1.30 or 1.29 with `TranslateStreamCloseWebsocketRequests=true` feature flag. Version below 1.29 are not supported. + +## Installation +See the [Getting started](https://github.com/Adyen/kubectl-rexec/blob/master/STARTED.md) guide. + +## Usage +See the [Getting started](https://github.com/Adyen/kubectl-rexec/blob/master/STARTED.md) guide. + +## Documentation +See the [Design](https://github.com/Adyen/kubectl-rexec/blob/master/DESIGN.md). + +## Support +If you have a feature request, or spotted a bug or a technical problem, create a GitHub issue. + +## License +MIT license. For more information, see the LICENSE file. \ No newline at end of file diff --git a/STARTED.md b/STARTED.md new file mode 100644 index 0000000..945cde8 --- /dev/null +++ b/STARTED.md @@ -0,0 +1,19 @@ +# Getting started + +For a proper installation you should use tagged images and your own implementation of kubernetes manifests, for a quick start however feel free to follow the instruction below. + +## Installing proxy + +The following command is going to install the proxy component, while adding a webhook that disables normal kubectl exec. + +``` +kustomize build manifests/ | kubectl -n kube-system apply -f - +``` + +## Installing the plugin + +Ensure that you go bin directory is in the path. + +``` +go install github.com/adyen/kubectl-rexec@latest +``` \ No newline at end of file diff --git a/diagram.png b/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..7a051c761aacfdc220aa4ca33c72f90beb0218a8 GIT binary patch literal 100139 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 literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..09e45f2 --- /dev/null +++ b/go.mod @@ -0,0 +1,91 @@ +module github.com/adyen/kubectl-rexec + +go 1.23.1 + +require ( + github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 + github.com/rs/zerolog v1.33.0 + github.com/spf13/cobra v1.8.1 + k8s.io/api v0.32.0 + k8s.io/apimachinery v0.32.0 + k8s.io/cli-runtime v0.31.4 + k8s.io/client-go v0.32.0 + k8s.io/component-base v0.31.4 + k8s.io/kubectl v0.31.4 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/daviddengcn/go-colortext v1.0.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/camelcase v1.0.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/lithammer/dedent v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/component-helpers v0.32.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/metrics v0.32.0 // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/kustomize/api v0.18.0 // indirect + sigs.k8s.io/kustomize/kustomize/v5 v5.5.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..33d52c9 --- /dev/null +++ b/go.sum @@ -0,0 +1,262 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE= +github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= +github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A= +github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE= +github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrSEUQ= +github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= +k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= +k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= +k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/cli-runtime v0.31.4 h1:iczCWiyXaotW+hyF5cWP8RnEYBCzZfJUF6otJ2m9mw0= +k8s.io/cli-runtime v0.31.4/go.mod h1:0/pRzAH7qc0hWx40ut1R4jLqiy2w/KnbqdaAI2eFG8U= +k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= +k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= +k8s.io/component-base v0.31.4 h1:wCquJh4ul9O8nNBSB8N/o8+gbfu3BVQkVw9jAUY/Qtw= +k8s.io/component-base v0.31.4/go.mod h1:G4dgtf5BccwiDT9DdejK0qM6zTK0jwDGEKnCmb9+u/s= +k8s.io/component-helpers v0.32.0 h1:pQEEBmRt3pDJJX98cQvZshDgJFeKRM4YtYkMmfOlczw= +k8s.io/component-helpers v0.32.0/go.mod h1:9RuClQatbClcokXOcDWSzFKQm1huIf0FzQlPRpizlMc= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/kubectl v0.31.4 h1:c8Af8xd1VjyoKyWMW0xHv2+tYxEjne8s6OOziMmaD10= +k8s.io/kubectl v0.31.4/go.mod h1:0E0rpXg40Q57wRE6LB9su+4tmwx1IzZrmIEvhQPk0i4= +k8s.io/metrics v0.32.0 h1:70qJ3ZS/9DrtH0UA0NVBI6gW2ip2GAn9e7NtoKERpns= +k8s.io/metrics v0.32.0/go.mod h1:skdg9pDjVjCPIQqmc5rBzDL4noY64ORhKu9KCPv1+QI= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= +sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= +sigs.k8s.io/kustomize/kustomize/v5 v5.5.0 h1:o1mtt6vpxsxDYaZKrw3BnEtc+pAjLz7UffnIvHNbvW0= +sigs.k8s.io/kustomize/kustomize/v5 v5.5.0/go.mod h1:AeFCmgCrXzmvjWWaeZCyBp6XzG1Y0w1svYus8GhJEOE= +sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= +sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/main.go b/main.go new file mode 100644 index 0000000..061c588 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import plugin "github.com/adyen/kubectl-rexec/plugin" + +func main() { + plugin.Rexec() +} diff --git a/manifests/apiservice.yaml b/manifests/apiservice.yaml new file mode 100644 index 0000000..24092d3 --- /dev/null +++ b/manifests/apiservice.yaml @@ -0,0 +1,14 @@ +apiVersion: apiregistration.k8s.io/v1 +kind: APIService +metadata: + name: v1beta1.audit.adyen.internal +spec: + group: audit.adyen.internal + groupPriorityMinimum: 100 + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURvakNDQW9xZ0F3SUJBZ0lVYnl6UTloVFViV3dwTFAwS05adk9uQ3l5emZVd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0h6RWRNQnNHQTFVRUF4TVVaSFZ0YlhrdVlXUjVaVzR1YVc1MFpYSnVZV3d3SGhjTk1qUXhNakUyTVRNeQpOekl5V2hjTk16UXhNakUwTVRNeU56UTJXakEyTVRRd01nWURWUVFERXl0a2RXMXRlUzVoWkhsbGJpNXBiblJsCmNtNWhiQ0JKYm5SbGNtMWxaR2xoZEdVZ1FYVjBhRzl5YVhSNU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0MKQVE4QU1JSUJDZ0tDQVFFQTdob2hKa0Z2d2ZpTldaK29RZ0Y1K0E3aEgrMnhsaFdhcm9rZDQwWFRoQlQ5dEMwVQplVkxZajF3MGR1UERhMXhqdUllQjhnQmVRTGpKbVlkU2oxblhHSUZ3Nzl6Qm5SMUhwK2FZRjNxRllLb1VLZ0tpCkp4bnNMa3NsZXE2amtBWmVpVFI5ZUdrNGQrU2xrZHB2VlZ1c2ZYV2hsZkhPeVRJWld1YW9NaEZXWC9RdThVNzcKMUxLdFUyT2dtWC9uOHEwZ2JtdmYwWUYvMGNnWExIR1Z2QUhvYjRTTmp5cjRkejdqZUhubVkza1E5UU5tY21nOQpidmNaenBYZXNSdzJjc013T1BhTHM2NmJleldRcW4vMWtYNUxUUTJyK2RCSWdQOEU5aW9YbnZ6QUpHSUZSOHV0CjNYekd0ejNjOW1URERPNlpHajVlbGV4eExIYjR3SkhXTW1UUUVRSURBUUFCbzRHK01JRzdNQTRHQTFVZER3RUIKL3dRRUF3SUJCakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlRyWEhvVWZXbnhOLzQrZWFoagp2UUQvMnRsL2R6QWZCZ05WSFNNRUdEQVdnQlRiSUxsZ1JHUEYrelFTWDdxOFIwb1RvWVAwVnpCWUJnZ3JCZ0VGCkJRY0JBUVJNTUVvd1NBWUlLd1lCQlFVSE1BS0dQR2gwZEhCek9pOHZkbUYxYkhRdGNHdHBMbTFoY25SdmJpNWwKZUhRdVpYVXVZV1I1Wlc0dWMyRnVaR0p2ZURvNE1qQXdMM1l4TDNCcmFTOWpZVEFOQmdrcWhraUc5dzBCQVFzRgpBQU9DQVFFQURxSW1jVm5UVFVaZEJoQVRrejVnYWdubHArY1EvUmY3MW5YNitnanZEcnRVSFg4bERpbHJMVC9oCitrQXZNZmNuS0VNQWJvMmVvU2VwY1NOY25sUDNBSEUzMHhBRzFRVGpFVTRuN0pxTGp0QmFIK0ZvSXd5QTRhRGsKNGpwZVZTUm5OWGNzanZRYmpLdTgzd3pTZ2J5OVJkNG00ajVvMVN3VXAwekZLVGNGWkx1bG84RUw3ZG9aNm1YMQpiY3I1WlJlYjJGanhQaHplVTVKU1EvUVhneGIwSjFFUjVzTER1ZmRweElCUVlpeGtnZkhpOGZxYkZKc0F0VmcxCmNHN2RXZWQ2ZWF3MXNLZk1aU3hUUy9HZEtRTDNob3J4MHNvQlhLdEY4REYrOXRsZ2tVZnk2VUVSYm43RzViankKMUptTTFmU2dOZXprR2l4UXlTODdabEk1cXhRTVpRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRFVqQ0NBanFnQXdJQkFnSVVVSytLNUkrWCtYM3lNaXdpNTBHYXZYdnlmY0V3RFFZSktvWklodmNOQVFFTApCUUF3SHpFZE1Cc0dBMVVFQXhNVVpIVnRiWGt1WVdSNVpXNHVhVzUwWlhKdVlXd3dIaGNOTWpReE1qRTJNVE15Ck56RTJXaGNOTXpReE1qRTBNVE15TnpRMldqQWZNUjB3R3dZRFZRUURFeFJrZFcxdGVTNWhaSGxsYmk1cGJuUmwKY201aGJEQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5uSElGS21UejZ1eGlnSgpKaGt4RWRrcXp3L0x3aDBNVXN5a0IvcXM0V0FsejhaL21DUi9BSVlyVTY4N21adlhvcDN0UHhjZlpnRUkzOW4wCmNyUU9UN0ZzWk9vekZCRjJhOVE2TWoycVFnb1Rodkc3K0VSY1pvcHIvYTdDWXcyRzd6MytrQ2FOUXdvUHpib1UKQlZQRjk3c3lVNFpIdUVPS3pjZk9YbWRlNm1jdG9PZXRaOWd1VnNHeEMwMlNpQUR3ZVdTZ2Y0bVY3RlBmTTI5TgpPcndGeUd4c05vUzNrcjViNy9WejhwQzhCUHpDRjUxRDIwZXJ2SmZMVmRVc3ZLQkR1cXdxdFZTUFE5eGxuWkFoCnVFQXpjRE1ua2xLU3NHYmp0dkZKbmJJTWVqWldybXZSS0g2YURtQW01bXNrRHRjYmc2NDk1NHcyMkROWVFDRnIKcGxXRFM2RUNBd0VBQWFPQmhUQ0JnakFPQmdOVkhROEJBZjhFQkFNQ0FRWXdEd1lEVlIwVEFRSC9CQVV3QXdFQgovekFkQmdOVkhRNEVGZ1FVMnlDNVlFUmp4ZnMwRWwrNnZFZEtFNkdEOUZjd0h3WURWUjBqQkJnd0ZvQVUyeUM1CllFUmp4ZnMwRWwrNnZFZEtFNkdEOUZjd0h3WURWUjBSQkJnd0ZvSVVaSFZ0YlhrdVlXUjVaVzR1YVc1MFpYSnUKWVd3d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFESzltTkhSNW9UNmRySGpZQ0xPNG5pZHhhdldBbzF0YTJEagpWcURGUGlaUEhPTGVxcVE3YVpHb3YzRzh3Y0I1cnB0UCtRaGFxU2x5eHRUSHQ3MVhtdUdzelh3MjZTSHNzQUROCkpBQ0dHVUNXcDd3QitxVUJjbHVYTDljUmsyOHhHczYzcGJWMHlmZEVXK3NTeTlhVGJTcHhaallwb2tUcU5NQnQKNFRySGQ3TWxoM2djaU10MFJVUVVJaVhGYmx0N1RLSGI1eW13OUlDd1pkUGZ1V214VnNqY2ZGVHozK3lqT09UbwpaWkJOQmY5TVkrWlg5ZjNzRzFhOFFzK3dHUUtTREFaSlNlTWJlU25JMG5CNTlKYm5ZR2NmWVM2M2lIampIM1dhClp1Q0FRS2V6bkNpbUI2M1RoY2psaWduZkJpWksybE4xZThQZ3JKZmlkaGZUZWpuSzBNRT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + service: + name: rexec + namespace: kube-system + port: 8443 + version: v1beta1 + versionPriority: 100 \ No newline at end of file diff --git a/manifests/deployment.yaml b/manifests/deployment.yaml new file mode 100644 index 0000000..33d3d33 --- /dev/null +++ b/manifests/deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: rexec + name: rexec + namespace: kube-system +spec: + replicas: 1 + selector: + matchLabels: + app: rexec + strategy: {} + template: + metadata: + labels: + app: rexec + spec: + serviceAccountName: rexec-impersonator + automountServiceAccountToken: true + containers: + - image: ghcr.io/adyen/kubectl-rexec:latest + imagePullPolicy: Always + name: rexec + ports: + - containerPort: 8443 + args: + - --audit-trace + - --by-pass-user=system:admin + resources: + requests: + ephemeral-storage: "1Gi" + cpu: 150m + memory: "128Mi" + limits: + ephemeral-storage: "1Gi" + cpu: 300m + memory: "256Mi" + volumeMounts: + - mountPath: /etc/pki/rexec + name: rexec-tls + readOnly: true + volumes: + - name: rexec-tls + secret: + secretName: rexec-tls diff --git a/manifests/kustomization.yaml b/manifests/kustomization.yaml new file mode 100644 index 0000000..61c0c00 --- /dev/null +++ b/manifests/kustomization.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kube-system +resources: + - apiservice.yaml + - deployment.yaml + - rbac.yaml + - secrets.yaml + - service.yaml + - webhook.yaml \ No newline at end of file diff --git a/manifests/rbac.yaml b/manifests/rbac.yaml new file mode 100644 index 0000000..ecb5db7 --- /dev/null +++ b/manifests/rbac.yaml @@ -0,0 +1,30 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: rexec-impersonator +rules: +- apiGroups: [""] + resources: ["users", "groups"] + verbs: ["impersonate"] +- apiGroups: ["authentication.k8s.io"] + resources: ["userextras/secret-sauce"] + verbs: ["impersonate"] +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: rexec-impersonator +automountServiceAccountToken: true +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: rexec-impersonator +subjects: +- kind: ServiceAccount + name: rexec-impersonator + namespace: kube-system +roleRef: + kind: ClusterRole + name: rexec-impersonator + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/manifests/secrets.yaml b/manifests/secrets.yaml new file mode 100644 index 0000000..24a5fab --- /dev/null +++ b/manifests/secrets.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: rexec-tls + namespace: kube-system +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQwekNDQXJ1Z0F3SUJBZ0lVUlZwdzQ2YUdCek1na3MrVHBOeTNkcGhYSVc0d0RRWUpLb1pJaHZjTkFRRUwKQlFBd05qRTBNRElHQTFVRUF4TXJaSFZ0YlhrdVlXUjVaVzR1YVc1MFpYSnVZV3dnU1c1MFpYSnRaV1JwWVhSbApJRUYxZEdodmNtbDBlVEFlRncweU5ERXlNVFl4TXpNd05EaGFGdzB6TkRFeU1EUXhNek14TVRoYU1CQXhEakFNCkJnTlZCQU1UQlhKbGVHVmpNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXJyY0gKY3RUR002OVBtLzFqT3M5VmVZTjdhcWkvN1JWNU1UTHc0MWZXSElzb3BzOFk0Y0hIVmZNNEJZcW1rK0pVWlMvVwpzWnRvUWJiTG1HNWxpWU5PNVRrWERjdVVtb1dibzREcE5JeXZsS2Y4S2M3VVpCUGRKcW9DTkR3ekZvT082NG90Ck9uS0c4eUtGT1B6QU1mSHp1UFlodklaNWptK3ljRWFCWWxkSjQ2UUxKYStta0xVSHFUU3ZGZzluV0doM2VpWVgKRzNyRUQ0b0NSWkJ6THg0Nml0UjFGaXJmeG8xVmNybEhRbm43SHFUTDBCODZRY1RQYWZheWlRWWMvWHRZeHVoNwpjWTlLcXR4ME9yZVNiZUtlUTZDdFYwZS9CL21iK3ROTzUvVTd0bi9WanZlR0p6NUp0UmlDWEdpRFZaL21oOEwwCmVDdXc0K3c4dm43a2xpOHFTUUlEQVFBQm80SCtNSUg3TUE0R0ExVWREd0VCL3dRRUF3SURxREFkQmdOVkhTVUUKRmpBVUJnZ3JCZ0VGQlFjREFRWUlLd1lCQlFVSEF3SXdIUVlEVlIwT0JCWUVGTGxyN3BxbmIzQ1JJVkFTRTV2YQpvNlBzbUpJV01COEdBMVVkSXdRWU1CYUFGT3RjZWhSOWFmRTMvajU1cUdPOUFQL2EyWDkzTUlHSkJnTlZIUkVFCmdZRXdmNElKYkc5allXeG9iM04wZ2dWeVpYaGxZNElSY21WNFpXTXVhM1ZpWlMxemVYTjBaVzJDRlhKbGVHVmoKTG10MVltVXRjM2x6ZEdWdExuTjJZNElpY21WNFpXTXVhM1ZpWlMxemVYTjBaVzB1YzNaakxtTnNkWE5sY2k1cwpiMk5oYklJZGNtVjRaV011YTNWaVpTMXplWE4wWlcwdWMzWmpMbU5zZFhOMFpYSXdEUVlKS29aSWh2Y05BUUVMCkJRQURnZ0VCQU9LMzNwdzhycitrMGtlb0ltQ2dIVUtvR0pjSmxUanlOdUVHLzF3TzIrNWU0OHZkb0pwU0NYdFEKczBCcHBjZWcvSyt0SXZHYWNHRVZ4UjUrVVRFYlhSTExabXlBeG12QVJheFhmRWtmOHJhS1lKbDAyMmFaU2ttago3YnNHOUY1amFuQXF1L2ZlZ1N2V0h1cy9RaGM1dHBrVHFUOEM2WURaelgwTXdGb0VOK1h2a1ZaNDN4aXhlQWhoCkpXVzhiaFV3L3lkV25yVFo4RkM4bzhWTWczTDFBeityckJ4ZnJUS3hVRHJqdWpEanJTRk55SElFN0hjOHB5bVgKeDBTdWVSbmw1TDFIaG5ydjRFTUNabk9jbEVjWFU0NDZOUE9uUC9NRlhXSHRxZ1Mzd0lNbjQ2VU5HN0IrcHlHdgpBL2t4Nm1pNEhXTzBURzNYVmM0bUpPTmY1WHhOdXNRPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBcnJjSGN0VEdNNjlQbS8xak9zOVZlWU43YXFpLzdSVjVNVEx3NDFmV0hJc29wczhZCjRjSEhWZk00QllxbWsrSlVaUy9Xc1p0b1FiYkxtRzVsaVlOTzVUa1hEY3VVbW9XYm80RHBOSXl2bEtmOEtjN1UKWkJQZEpxb0NORHd6Rm9PTzY0b3RPbktHOHlLRk9QekFNZkh6dVBZaHZJWjVqbSt5Y0VhQllsZEo0NlFMSmErbQprTFVIcVRTdkZnOW5XR2gzZWlZWEczckVENG9DUlpCekx4NDZpdFIxRmlyZnhvMVZjcmxIUW5uN0hxVEwwQjg2ClFjVFBhZmF5aVFZYy9YdFl4dWg3Y1k5S3F0eDBPcmVTYmVLZVE2Q3RWMGUvQi9tYit0Tk81L1U3dG4vVmp2ZUcKSno1SnRSaUNYR2lEVlovbWg4TDBlQ3V3NCt3OHZuN2tsaThxU1FJREFRQUJBb0lCQVFDUW16YlVDVjMrKzFRVgoxUlNqWVdYcWpEUERKT2F0c1Q4OHhGL3ltd25CV0VDT1NBemRGZ2tKajZSSG1lbWpyd21STXBZdExHYVBOVit2CnkzZkk2R0NOZ3NJZERlbnlOekdKazdIeFo1d1Btell2MkZ1Y2RZQnVkdm9hQjlWMUJmQnQ3VkRmOWxqUnRqbXoKNENhbmNBMzhnZU9NYVhVRXVsaGphMGU5Z0dmTXUrU3dIMW9oTlpJVHpncXZsMGNqb3VUS2ZUQXdPTkYrSHJQagpBdUV5ZXJqREhVcStXRVg2Ty82MFZBaGM5cnBwd05vSHhjM1ZMeUk2cUhTaDZPeGt5d0x3SmsvU1NKLzIvRFQrClBPRTBGYXRkNFRBUzBPSkpqTzk0cWUyZy8weEtNbkV2R2gzTktlQUppTS90L2VBNEhPOTVUeXA2U0kwOERCUnIKNktvN05XbHhBb0dCQU5sOCtQRzRScG5LQVpYckEvNk1yRDN4ZDlDcFFqc202c1QxdGd2Y0NaY1B2UlJBc3dscAprcEVrRGRteEdCSHJZaDdrM3JUNk5jNjRlZm5vTVV2eVQ2anNXK2tNMy9XU0dmcG5BeTVjRXgxQmFNK2haN1d2CnpHTE5LZnFzanZrdlV2NExaeUpaMXgvVXhObXdLZ2NJZjRJYm95SjNFS1dOVFppM0JTdGRpQjJsQW9HQkFNMm4KRndvcjROanpibm9PZWtNblN4L2k1aHoyNjBoWW5xQTlJSk93M2ZGeGJPOGZ0ZVcxYUxVN1Q3MHIrVnpiMjlWegpxbE1GaHpmaVdGQkhQM0FxOE1CRWN1aEdxRVZYRXo3ZE53MlByaWlVMnZRVGZ3SDhGRWZhYXI0SXdUdE9Dc2k4CkRoZ2lIREt6SndVR1paNWhmL2s5eFdMZlAyOFdRaUxpZ0tNTjdJRFZBb0dCQUp0b2I4TForS2ovN2U0Z2h6UW4KZFJTMkxQV1BYT0pEeHRLQytWaTBISzR5OHRzNytETXJteTNYWTRaQXc0QmFnRHl2TW15RHRsdEcrdklXZHROYwpESXdhaVBxWTFwZjFsRmFYc1hBNUh2ZHl1K0JSNTNldWJRL1Vwc0NXK1hzWjArWHdZL3ZwMG96T1R2TjJyREZtCll5YW5kUVMxcTlHQWpRZ3BENnFUSlNaNUFvR0FicEc3elhneDkvTktIczNSNW5FbDd3cnJkZjg4R1RXc2M3THAKNVA1ZkZnVko4SGM0TVQwTUF3VFVwbjBTSVY4RUh3dUZOQVh3NFpjTXJIemlHc2k3a0dRODg2MnBvejVoMXBiUgpsclQ5aWt3ZVBNU09zTjU3ZVBaeUZhSlhZaTlmbFBXbkRrcW9wb20wSFB1SGYxUWtuamtiKzBEVXRrRmRaYXdxClJZQ2krOUVDZ1lCd2M2aW0yNFBqbm5SZEx3QjVwK3NkbUl2ZllSTTBzZCsyLzllTWZ2clE2eTUxYjRLQ2NvUEMKbXhqNndHbndUZEhFbXBPMTd3TTNGd0VQVHBYSUxFeHo4U3BTaWxBTS8wWkVYZVg5eHRTdHo5R215cDFxTGQ5aQoxa2tlRW5PL3V2cXVxWjQxMFJhcmluKytoZjZ2S04rZkNCVGd1dS8wUVhENVdFRi9rYlpWZmc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= diff --git a/manifests/service.yaml b/manifests/service.yaml new file mode 100644 index 0000000..898ba9b --- /dev/null +++ b/manifests/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: rexec + namespace: kube-system +spec: + sessionAffinity: ClientIP + ports: + - name: rexec + port: 8443 + protocol: TCP + targetPort: 8443 + selector: + app: rexec + type: ClusterIP \ No newline at end of file diff --git a/manifests/webhook.yaml b/manifests/webhook.yaml new file mode 100644 index 0000000..5cb6604 --- /dev/null +++ b/manifests/webhook.yaml @@ -0,0 +1,21 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: deny-pod-exec +webhooks: +- name: deny-pod-exec.k8s.io + clientConfig: + service: + name: rexec + namespace: kube-system + port: 8443 + path: /validate-exec + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURvakNDQW9xZ0F3SUJBZ0lVYnl6UTloVFViV3dwTFAwS05adk9uQ3l5emZVd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0h6RWRNQnNHQTFVRUF4TVVaSFZ0YlhrdVlXUjVaVzR1YVc1MFpYSnVZV3d3SGhjTk1qUXhNakUyTVRNeQpOekl5V2hjTk16UXhNakUwTVRNeU56UTJXakEyTVRRd01nWURWUVFERXl0a2RXMXRlUzVoWkhsbGJpNXBiblJsCmNtNWhiQ0JKYm5SbGNtMWxaR2xoZEdVZ1FYVjBhRzl5YVhSNU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0MKQVE4QU1JSUJDZ0tDQVFFQTdob2hKa0Z2d2ZpTldaK29RZ0Y1K0E3aEgrMnhsaFdhcm9rZDQwWFRoQlQ5dEMwVQplVkxZajF3MGR1UERhMXhqdUllQjhnQmVRTGpKbVlkU2oxblhHSUZ3Nzl6Qm5SMUhwK2FZRjNxRllLb1VLZ0tpCkp4bnNMa3NsZXE2amtBWmVpVFI5ZUdrNGQrU2xrZHB2VlZ1c2ZYV2hsZkhPeVRJWld1YW9NaEZXWC9RdThVNzcKMUxLdFUyT2dtWC9uOHEwZ2JtdmYwWUYvMGNnWExIR1Z2QUhvYjRTTmp5cjRkejdqZUhubVkza1E5UU5tY21nOQpidmNaenBYZXNSdzJjc013T1BhTHM2NmJleldRcW4vMWtYNUxUUTJyK2RCSWdQOEU5aW9YbnZ6QUpHSUZSOHV0CjNYekd0ejNjOW1URERPNlpHajVlbGV4eExIYjR3SkhXTW1UUUVRSURBUUFCbzRHK01JRzdNQTRHQTFVZER3RUIKL3dRRUF3SUJCakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlRyWEhvVWZXbnhOLzQrZWFoagp2UUQvMnRsL2R6QWZCZ05WSFNNRUdEQVdnQlRiSUxsZ1JHUEYrelFTWDdxOFIwb1RvWVAwVnpCWUJnZ3JCZ0VGCkJRY0JBUVJNTUVvd1NBWUlLd1lCQlFVSE1BS0dQR2gwZEhCek9pOHZkbUYxYkhRdGNHdHBMbTFoY25SdmJpNWwKZUhRdVpYVXVZV1I1Wlc0dWMyRnVaR0p2ZURvNE1qQXdMM1l4TDNCcmFTOWpZVEFOQmdrcWhraUc5dzBCQVFzRgpBQU9DQVFFQURxSW1jVm5UVFVaZEJoQVRrejVnYWdubHArY1EvUmY3MW5YNitnanZEcnRVSFg4bERpbHJMVC9oCitrQXZNZmNuS0VNQWJvMmVvU2VwY1NOY25sUDNBSEUzMHhBRzFRVGpFVTRuN0pxTGp0QmFIK0ZvSXd5QTRhRGsKNGpwZVZTUm5OWGNzanZRYmpLdTgzd3pTZ2J5OVJkNG00ajVvMVN3VXAwekZLVGNGWkx1bG84RUw3ZG9aNm1YMQpiY3I1WlJlYjJGanhQaHplVTVKU1EvUVhneGIwSjFFUjVzTER1ZmRweElCUVlpeGtnZkhpOGZxYkZKc0F0VmcxCmNHN2RXZWQ2ZWF3MXNLZk1aU3hUUy9HZEtRTDNob3J4MHNvQlhLdEY4REYrOXRsZ2tVZnk2VUVSYm43RzViankKMUptTTFmU2dOZXprR2l4UXlTODdabEk1cXhRTVpRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRFVqQ0NBanFnQXdJQkFnSVVVSytLNUkrWCtYM3lNaXdpNTBHYXZYdnlmY0V3RFFZSktvWklodmNOQVFFTApCUUF3SHpFZE1Cc0dBMVVFQXhNVVpIVnRiWGt1WVdSNVpXNHVhVzUwWlhKdVlXd3dIaGNOTWpReE1qRTJNVE15Ck56RTJXaGNOTXpReE1qRTBNVE15TnpRMldqQWZNUjB3R3dZRFZRUURFeFJrZFcxdGVTNWhaSGxsYmk1cGJuUmwKY201aGJEQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU5uSElGS21UejZ1eGlnSgpKaGt4RWRrcXp3L0x3aDBNVXN5a0IvcXM0V0FsejhaL21DUi9BSVlyVTY4N21adlhvcDN0UHhjZlpnRUkzOW4wCmNyUU9UN0ZzWk9vekZCRjJhOVE2TWoycVFnb1Rodkc3K0VSY1pvcHIvYTdDWXcyRzd6MytrQ2FOUXdvUHpib1UKQlZQRjk3c3lVNFpIdUVPS3pjZk9YbWRlNm1jdG9PZXRaOWd1VnNHeEMwMlNpQUR3ZVdTZ2Y0bVY3RlBmTTI5TgpPcndGeUd4c05vUzNrcjViNy9WejhwQzhCUHpDRjUxRDIwZXJ2SmZMVmRVc3ZLQkR1cXdxdFZTUFE5eGxuWkFoCnVFQXpjRE1ua2xLU3NHYmp0dkZKbmJJTWVqWldybXZSS0g2YURtQW01bXNrRHRjYmc2NDk1NHcyMkROWVFDRnIKcGxXRFM2RUNBd0VBQWFPQmhUQ0JnakFPQmdOVkhROEJBZjhFQkFNQ0FRWXdEd1lEVlIwVEFRSC9CQVV3QXdFQgovekFkQmdOVkhRNEVGZ1FVMnlDNVlFUmp4ZnMwRWwrNnZFZEtFNkdEOUZjd0h3WURWUjBqQkJnd0ZvQVUyeUM1CllFUmp4ZnMwRWwrNnZFZEtFNkdEOUZjd0h3WURWUjBSQkJnd0ZvSVVaSFZ0YlhrdVlXUjVaVzR1YVc1MFpYSnUKWVd3d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFESzltTkhSNW9UNmRySGpZQ0xPNG5pZHhhdldBbzF0YTJEagpWcURGUGlaUEhPTGVxcVE3YVpHb3YzRzh3Y0I1cnB0UCtRaGFxU2x5eHRUSHQ3MVhtdUdzelh3MjZTSHNzQUROCkpBQ0dHVUNXcDd3QitxVUJjbHVYTDljUmsyOHhHczYzcGJWMHlmZEVXK3NTeTlhVGJTcHhaallwb2tUcU5NQnQKNFRySGQ3TWxoM2djaU10MFJVUVVJaVhGYmx0N1RLSGI1eW13OUlDd1pkUGZ1V214VnNqY2ZGVHozK3lqT09UbwpaWkJOQmY5TVkrWlg5ZjNzRzFhOFFzK3dHUUtTREFaSlNlTWJlU25JMG5CNTlKYm5ZR2NmWVM2M2lIampIM1dhClp1Q0FRS2V6bkNpbUI2M1RoY2psaWduZkJpWksybE4xZThQZ3JKZmlkaGZUZWpuSzBNRT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + rules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CONNECT"] + resources: ["pods/exec"] + admissionReviewVersions: ["v1"] + sideEffects: None + failurePolicy: Fail diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 0000000..59d632b --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,206 @@ +package plugin + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" + _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + cliflag "k8s.io/component-base/cli/flag" + "k8s.io/kubectl/pkg/cmd" + cmdexec "k8s.io/kubectl/pkg/cmd/exec" + "k8s.io/kubectl/pkg/cmd/plugin" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/cmd/util/podcmd" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/kubectl/pkg/util/completion" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +// We dont do much here, mostly implementing the same exec command +// as in upstream, with the difference in the path we are calling + +var MatchVersionKubeConfigFlags *cmdutil.MatchVersionFlags + +const ( + defaultPodExecTimeout = 60 * time.Second +) + +func Rexec() { + ioStreams := genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} + warningsAsErrors := false + + kubectlOptions := cmd.KubectlOptions{ + + PluginHandler: cmd.NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes), + Arguments: os.Args, + ConfigFlags: genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag().WithDiscoveryBurst(300).WithDiscoveryQPS(50.0).WithWarningPrinter(ioStreams), + IOStreams: ioStreams, + } + + cmds := &cobra.Command{ + Use: "rexec", + Short: i18n.T("rexec plugin for kubectl exec"), + Long: templates.LongDesc(` + provides audited way to perform kubectl exec.`), + } + + cmds.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc) + + flags := cmds.PersistentFlags() + + flags.BoolVar(&warningsAsErrors, "warnings-as-errors", warningsAsErrors, "Treat warnings received from the server as errors and exit with a non-zero exit code") + + kubectlOptions.ConfigFlags.AddFlags(flags) + + MatchVersionKubeConfigFlags = cmdutil.NewMatchVersionFlags(kubectlOptions.ConfigFlags) + MatchVersionKubeConfigFlags.AddFlags(flags) + + f := cmdutil.NewFactory(MatchVersionKubeConfigFlags) + + originalExec := cmdexec.NewCmdExec(f, kubectlOptions.IOStreams) + + options := &cmdexec.ExecOptions{ + StreamOptions: cmdexec.StreamOptions{ + IOStreams: kubectlOptions.IOStreams, + }, + + Executor: &cmdexec.DefaultRemoteExecutor{}, + } + + roptions := &RexecOptoins{ + ExecOptions: options, + } + + newExec := &cobra.Command{ + Use: originalExec.Use, + DisableFlagsInUseLine: originalExec.DisableFlagsInUseLine, + Short: originalExec.Short, + Long: originalExec.Long, + Example: originalExec.Example, + ValidArgsFunction: originalExec.ValidArgsFunction, + Run: func(cmd *cobra.Command, args []string) { + argsLenAtDash := cmd.ArgsLenAtDash() + cmdutil.CheckErr(roptions.ExecOptions.Complete(f, cmd, args, argsLenAtDash)) + cmdutil.CheckErr(roptions.ExecOptions.Validate()) + cmdutil.CheckErr(roptions.rexecRun()) + }, + } + + cmdutil.AddPodRunningTimeoutFlag(newExec, defaultPodExecTimeout) + cmdutil.AddJsonFilenameFlag(newExec.Flags(), &options.FilenameOptions.Filenames, "to use to exec into the resource") + + cmdutil.AddContainerVarFlags(newExec, &options.ContainerName, options.ContainerName) + cmdutil.CheckErr(newExec.RegisterFlagCompletionFunc("container", completion.ContainerCompletionFunc(f))) + + newExec.Flags().BoolVarP(&roptions.ExecOptions.Stdin, "stdin", "i", roptions.ExecOptions.Stdin, "Pass stdin to the container") + newExec.Flags().BoolVarP(&roptions.ExecOptions.TTY, "tty", "t", roptions.ExecOptions.TTY, "Stdin is a TTY") + newExec.Flags().BoolVarP(&roptions.ExecOptions.Quiet, "quiet", "q", roptions.ExecOptions.Quiet, "Only print output from the remote session") + + cmds.AddCommand(newExec) + + cmds.Execute() +} + +type RexecOptoins struct { + *cmdexec.ExecOptions +} + +func NewRexecOptions(e *cmdexec.ExecOptions) *RexecOptoins { + r := RexecOptoins{e} + return &r +} + +// mostly copy paste of the upstream Run() command +// with the minimal adjustment to call a different +// endpoint +func (r *RexecOptoins) rexecRun() error { + var err error + if len(r.PodName) != 0 { + r.Pod, err = r.PodClient.Pods(r.ExecOptions.Namespace).Get(context.TODO(), r.ExecOptions.PodName, metav1.GetOptions{}) + if err != nil { + return err + } + } else { + builder := r.ExecOptions.Builder(). + WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). + FilenameParam(r.ExecOptions.EnforceNamespace, &r.ExecOptions.FilenameOptions). + NamespaceParam(r.ExecOptions.Namespace).DefaultNamespace() + if len(r.ExecOptions.ResourceName) > 0 { + builder = builder.ResourceNames("pods", r.ExecOptions.ResourceName) + } + + obj, err := builder.Do().Object() + if err != nil { + return err + } + + if meta.IsListType(obj) { + return fmt.Errorf("cannot exec into multiple objects at a time") + } + + r.ExecOptions.Pod, err = r.ExecutablePodFn(MatchVersionKubeConfigFlags, obj, r.ExecOptions.GetPodTimeout) + if err != nil { + return err + } + } + + pod := r.ExecOptions.Pod + + if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { + return fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase) + } + + containerName := r.ExecOptions.ContainerName + if len(containerName) == 0 { + container, err := podcmd.FindOrDefaultContainerByName(pod, containerName, r.ExecOptions.Quiet, r.ExecOptions.ErrOut) + if err != nil { + return err + } + containerName = container.Name + } + + t := r.ExecOptions.SetupTTY() + + var sizeQueue remotecommand.TerminalSizeQueue + if t.Raw { + sizeQueue = t.MonitorSize(t.GetSize()) + + r.ExecOptions.ErrOut = nil + } + + fn := func() error { + restClient, err := restclient.RESTClientFor(r.Config) + if err != nil { + return err + } + + req := restClient.Post().RequestURI(fmt.Sprintf("apis/audit.adyen.internal/v1beta1/namespaces/%s/pods/%s/exec", pod.Namespace, pod.Name)) + req.VersionedParams(&corev1.PodExecOptions{ + Container: containerName, + Command: r.ExecOptions.Command, + Stdin: r.ExecOptions.Stdin, + Stdout: r.ExecOptions.Out != nil, + Stderr: r.ExecOptions.ErrOut != nil, + TTY: t.Raw, + }, scheme.ParameterCodec) + + return r.ExecOptions.Executor.Execute(req.URL(), r.ExecOptions.Config, r.ExecOptions.In, r.ExecOptions.Out, r.ExecOptions.ErrOut, t.Raw, sizeQueue) + } + + if err := t.Safe(fn); err != nil { + return err + } + + return nil +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..bbd0e77 --- /dev/null +++ b/renovate.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:base"], + "internalChecksFilter": "strict", + "updateNotScheduled": false, + "minimumReleaseAge": "21 days", + "pre-commit": { + "enabled": true + } +} \ No newline at end of file diff --git a/rexec/main.go b/rexec/main.go new file mode 100644 index 0000000..7ae6a9a --- /dev/null +++ b/rexec/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "github.com/adyen/kubectl-rexec/rexec/server" + "github.com/spf13/cobra" +) + +func main() { + + cmd := &cobra.Command{ + Use: "rexec-server", + Run: func(cmd *cobra.Command, args []string) { + server.Init() + server.Server() + }, + } + cmd.Flags().BoolVar(&server.AuditFullTraceLog, "audit-trace", false, "if set all keystrokes will be logged") + cmd.Flags().BoolVar(&server.SysDebugLog, "sys-debug", false, "if set more system logs will be produces") + cmd.Flags().StringArrayVar(&server.ByPassedUsers, "by-pass-user", []string{}, "allow user to bypass webhook restriction") + cmd.Flags().StringVar(&server.SecretSauce, "by-pass-shared-key", "", "shared key between apiservice and validatingwebhook") + cmd.Flags().IntVar(&server.MaxStokesPerLine, "max-strokes-per-line", 0, "set how much keystores can be held in the async audit before flush") + err := cmd.Execute() + if err != nil { + server.SysLogger.Fatal().Msg(err.Error()) + } +} diff --git a/rexec/server/async.go b/rexec/server/async.go new file mode 100644 index 0000000..78da5be --- /dev/null +++ b/rexec/server/async.go @@ -0,0 +1,52 @@ +package server + +// asyncAuditor will try to make merged commads out of keystrokes +func asyncAuditor() { + SysLogger.Debug().Msg("starting asyncAuditor") + for { + audit, ok := <-asyncAuditChan + if !ok { + SysLogger.Debug().Msg("channel closed, stopping asyncAuditor") + break + } + storeOrFlush(audit) + } +} + +// storeOrFlush will push keystrokes into a byte slice and +// flush it upen enter or a certain limit +func storeOrFlush(audit asyncAudit) { + for _, ascii := range audit.ascii { + switch ascii { + case 0: + // nothing + case 8, 127: + commandSync.Lock() + if len(commandMap[audit.ctxid]) > 0 { + commandMap[audit.ctxid] = commandMap[audit.ctxid][:len(commandMap[audit.ctxid])-1] + } + commandSync.Unlock() + case 13: + commandSync.Lock() + logCommand(string(commandMap[audit.ctxid]), userMap[audit.ctxid], audit.ctxid) + commandMap[audit.ctxid] = nil + commandSync.Unlock() + default: + commandSync.Lock() + // to prevent oom kills by shoving too much input into one line + // we flush after the amount of strokes set in MaxStokesPerLine + if len(commandMap[audit.ctxid]) > MaxStokesPerLine { + logCommand(string(commandMap[audit.ctxid]), userMap[audit.ctxid], audit.ctxid) + commandMap[audit.ctxid] = nil + } + commandMap[audit.ctxid] = append(commandMap[audit.ctxid], ascii) + commandSync.Unlock() + } + + } +} + +type asyncAudit struct { + ctxid string + ascii []byte +} diff --git a/rexec/server/config.go b/rexec/server/config.go new file mode 100644 index 0000000..c978a07 --- /dev/null +++ b/rexec/server/config.go @@ -0,0 +1,96 @@ +package server + +import ( + "crypto/x509" + "os" + "sync" + + "github.com/google/uuid" + "github.com/rs/zerolog" +) + +var token string +var proxyMap map[string]bool +var userMap map[string]string +var mapSync sync.Mutex +var SysLogger zerolog.Logger +var auditLogger zerolog.Logger +var SysDebugLog bool +var AuditFullTraceLog bool +var CAPool *x509.CertPool +var asyncAuditChan chan asyncAudit +var commandMap map[string][]byte +var commandSync sync.Mutex +var SecretSauce string +var ByPassedUsers []string +var MaxStokesPerLine int + +func Init() { + var sysLogLevel zerolog.Level + sysLogLevel = zerolog.FatalLevel + if SysDebugLog { + sysLogLevel = zerolog.DebugLevel + } + + var auditLogLevel zerolog.Level + auditLogLevel = zerolog.InfoLevel + if AuditFullTraceLog { + auditLogLevel = zerolog.TraceLevel + } + + logger := zerolog.New(os.Stdout).With().Timestamp().Logger() + + SysLogger = logger.With().Str("facility", "sys").Logger().Level(sysLogLevel) + auditLogger = logger.With().Str("facility", "audit").Logger().Level(auditLogLevel) + rawCaCert, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") + if err != nil { + logger.Fatal().Err(err) + } + CAPool = x509.NewCertPool() + CAPool.AppendCertsFromPEM(rawCaCert) + rawToken, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + logger.Fatal().Err(err) + } + token = string(rawToken) + proxyMap = make(map[string]bool) + userMap = make(map[string]string) + commandMap = make(map[string][]byte) + asyncAuditChan = make(chan asyncAudit) + + if SecretSauce == "" { + SecretSauce = uuid.New().String() + } + if SecretSauce != "" { + _, err = uuid.Parse(SecretSauce) + if err != nil { + logger.Fatal().Err(err) + } + } + if MaxStokesPerLine == 0 { + MaxStokesPerLine = 2000 + } + + go asyncAuditor() +} + +func logCommand(command, user, ctxid string) { + auditLogger.Info().Str("user", user).Str("session", ctxid).Str("command", command).Msg("") +} + +var httpSpec = ` +{ + "kind": "APIResourceList", + "apiVersion": "v1", + "groupVersion": "audit.adyen.internal/v1beta1", + "resources": [] +} +` + +var httpForbidden = ` +No User found +` + +var httpInternalError = ` +Internal errror +` diff --git a/rexec/server/parser.go b/rexec/server/parser.go new file mode 100644 index 0000000..b13a86f --- /dev/null +++ b/rexec/server/parser.go @@ -0,0 +1,68 @@ +package server + +import ( + "encoding/binary" + "errors" +) + +type webSocketFrame struct { + Fin bool + Opcode byte + Mask bool + Payload []byte +} + +// parseWebSocketFrame is for parsing websocket traffic +func parseWebSocketFrame(data []byte) (*webSocketFrame, error) { + if len(data) < 2 { + return nil, errors.New("data too short to be a WebSocket frame") + } + + fin := data[0]&0x80 != 0 + opcode := data[0] & 0x0F + + mask := data[1]&0x80 != 0 + payloadLen := int(data[1] & 0x7F) + + var offset int + switch payloadLen { + case 126: + if len(data) < 4 { + return nil, errors.New("data too short for extended payload length") + } + payloadLen = int(binary.BigEndian.Uint16(data[2:4])) + offset = 4 + case 127: + if len(data) < 10 { + return nil, errors.New("data too short for extended payload length") + } + payloadLen = int(binary.BigEndian.Uint64(data[2:10])) + offset = 10 + default: + offset = 2 + } + + if mask { + offset += 4 + } + + var maskingKey []byte + if mask { + maskingKey = data[offset-4 : offset] + } + + payload := data[offset:] + + if mask { + for i := 0; i < len(payload); i++ { + payload[i] ^= maskingKey[i%4] + } + } + + return &webSocketFrame{ + Fin: fin, + Opcode: opcode, + Mask: mask, + Payload: payload, + }, nil +} diff --git a/rexec/server/server.go b/rexec/server/server.go new file mode 100644 index 0000000..6fd445e --- /dev/null +++ b/rexec/server/server.go @@ -0,0 +1,258 @@ +package server + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "time" + + "github.com/google/uuid" + "github.com/gorilla/mux" + admissionv1 "k8s.io/api/admission/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Server() { + // creating a mux router + r := mux.NewRouter() + + // handling rexec request to handler + r.HandleFunc("/apis/audit.adyen.internal/v1beta1/namespaces/{namespace}/pods/{pod}/exec", rexecHandler) + // returning some dummy json making kubeapiserver happier + r.HandleFunc("/apis/audit.adyen.internal/v1beta1", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(httpSpec)) + }) + // handle native pod exec through a validating webhook + r.HandleFunc("/validate-exec", execHandler) + + // start tls listener + http.ListenAndServeTLS(":8443", "/etc/pki/rexec/tls.crt", "/etc/pki/rexec/tls.key", r) +} + +// rexecHandler is responsible for rewrite the request to an exec request +// and proxy it back to k8s api +func rexecHandler(w http.ResponseWriter, r *http.Request) { + // parsing for vars + pathParams := mux.Vars(r) + namespace := pathParams["namespace"] + pod := pathParams["pod"] + user := r.Header.Get("X-Remote-User") + + // if any of the mimimal parameters are missing we should bail + if user == "" || namespace == "" || pod == "" { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte(httpForbidden)) + return + } + r.Header.Add("Kubectl-Command", "kubectl exec") + + // adding the service account token we are using for impersonating + r.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + + // add user to impersonation header + r.Header.Add("Impersonate-User", user) + + // adding all passed groups as impersonation groups + groups := r.Header.Values("X-Remote-Group") + for _, group := range groups { + r.Header.Add("Impersonate-Group", group) + } + + // for the webhook service part we need to signal somehow + // that we are allowed to do execs, coming through this endpoint + // so we pass a custom shared key through the `Impersonate-Extra-Secret-Sauce` + // header which will end up in `admissionReview.Request.UserInfo.Extra` + r.Header.Add("Impersonate-Extra-Secret-Sauce", SecretSauce) + + // template old and new url pathes and replace them in the url + newPath := fmt.Sprintf("api/v1/namespaces/%s/pods/%s/exec", namespace, pod) + oldPath := fmt.Sprintf("apis/audit.adyen.internal/v1beta1/namespaces/%s/pods/%s/exec", namespace, pod) + r.URL.Path = strings.ReplaceAll(r.URL.Path, oldPath, newPath) + r.URL.RawPath = strings.ReplaceAll(r.URL.RawPath, oldPath, newPath) + r.Host = "kubernetes.default.svc.cluster.local:443" + + params, err := url.ParseQuery(r.URL.RawQuery) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(httpInternalError)) + return + } + + // first fetching the command parameters from the url params to check what commands were passed + // initially to the container + var initialCommand []string + needsRecording := false + for key, value := range params { + if key == "command" { + initialCommand = append(initialCommand, value...) + } + // we also check wether tty was requested, if so we will need to record the session + if key == "tty" { + needsRecording = true + } + } + + if !needsRecording { + // if we dont need any recording, we just pass the request back to the kube apiserver + url, _ := url.Parse("https://kubernetes.default.svc.cluster.local:443") + proxy := httputil.NewSingleHostReverseProxy(url) + + proxy.Transport = &http.Transport{ + DisableKeepAlives: true, + DisableCompression: true, + TLSClientConfig: &tls.Config{ + RootCAs: CAPool, + }, + } + + // Log initial command as an audit event + // as oneoff, since we dont do tty so there + // wont be a recording and a session id + logCommand(strings.Join(initialCommand, " "), user, "oneoff") + + proxy.FlushInterval = -1 + + proxy.ServeHTTP(w, r) + } else { + // in the case of recoding we will pass the request through a tcp proxy to make it easier + // to actually monitor what is being typed in to the shell + + // we begin to generate a uuid for the session and we set it as the id of a context + // we will use this id to keep track what use the session belongs to + ctxid := uuid.New().String() + ctx := context.WithValue(r.Context(), "sessionID", ctxid) + + // we save the session id into a map with the user's identity + mapSync.Lock() + userMap[ctxid] = user + mapSync.Unlock() + + // we set the previously generated context to the request + r.WithContext(ctx) + + // Log initial command as an audit event + // with sessin id + logCommand(strings.Join(initialCommand, " "), user, ctxid) + + // we start up a tcp forwarder for the session + go tcpForwarder(ctx) + + // we need to wait a bit until the listener is actually there + // probably there are 10 more sophisticated ways to do this + // but it is not important now + err = waitForListener(ctxid) + if err != nil { + SysLogger.Error().Err(err).Msg("waiting for listener") + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(httpInternalError)) + return + } + + // url does not really matter we are going through the socket anyway + url, _ := url.Parse("http://localhost:8080") + proxy := httputil.NewSingleHostReverseProxy(url) + + proxy.Transport = &http.Transport{ + DisableKeepAlives: true, + DisableCompression: true, + // we are forcing the reverse proxy to go through our socket + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", fmt.Sprintf("/%s", ctxid)) + }, + } + + proxy.FlushInterval = -1 + + proxy.ServeHTTP(w, r) + } +} + +// execHandler is responsible auditing exec request and allowing +// the ones coming through rexec api along with allowlisted users +func execHandler(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Content-Type") != "application/json" { + http.Error(w, "Invalid content type", http.StatusUnsupportedMediaType) + return + } + + var admissionReview admissionv1.AdmissionReview + if err := json.NewDecoder(r.Body).Decode(&admissionReview); err != nil { + http.Error(w, fmt.Sprintf("Failed to decode request: %v", err), http.StatusBadRequest) + return + } + + response := admissionv1.AdmissionResponse{ + UID: admissionReview.Request.UID, + } + + canPass := canPass(admissionReview) + + if admissionReview.Request.Kind.Kind == "PodExecOptions" { + response.Allowed = canPass + if !canPass { + response.Result = &metav1.Status{ + Message: "cannot use exec directly, use rexec plugin instead", + } + } + } else { + response.Allowed = true + } + admissionReview.Response = &response + respBytes, err := json.Marshal(admissionReview) + if err != nil { + http.Error(w, fmt.Sprintf("Failed to encode response: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(respBytes) +} + +// waitForListener is simly check wether the personnal tcp +// forwarder ready or not, if it is not there after 5 secs +// it bails +func waitForListener(listener string) error { + // again, super lazy but it is fine for now + for i := 0; i < 5; i++ { + if proxyMap[listener] { + SysLogger.Debug().Msgf("socket became ready on try %d", i) + return nil + } + SysLogger.Debug().Msgf("waiting for socket on try %d", i) + time.Sleep(1 * time.Second) + } + return errors.New("socket was not ready in time") +} + +// canPass checks wether the exec request is allowed +// or not +func canPass(rv admissionv1.AdmissionReview) bool { + // check for users that have a bypass for validating + for _, user := range ByPassedUsers { + if user == rv.Request.UserInfo.Username { + return true + } + } + + // we will check for shared key so we can validate the request was + // coming through the rexec endpoint + sauce, ok := rv.Request.UserInfo.Extra["secret-sauce"] + if ok { + if len(sauce) > 0 { + for _, sauce := range sauce { + if sauce == SecretSauce { + return true + } + } + } + } + return false +} diff --git a/rexec/server/tcp.go b/rexec/server/tcp.go new file mode 100644 index 0000000..d212c9a --- /dev/null +++ b/rexec/server/tcp.go @@ -0,0 +1,133 @@ +package server + +import ( + "context" + "crypto/tls" + "encoding/hex" + "fmt" + "io" + "net" + "os" + "strings" + + "github.com/rs/zerolog" +) + +const ( + targetAddress = "kubernetes.default.svc.cluster.local:443" +) + +func tcpForwarder(ctx context.Context) { + lc := net.ListenConfig{} + + ctxid := ctx.Value("sessionID").(string) + socketPath := fmt.Sprintf("/%s", ctxid) + + // we setup a unix listener for the specific session + listener, err := lc.Listen(ctx, "unix", socketPath) + if err != nil { + SysLogger.Error().Err(err).Msgf("failed to start listener for %d", ctxid) + return + } + defer listener.Close() + + SysLogger.Debug().Msgf("starting personal tcp forwarer at " + socketPath) + + // in a cheap manner we signer back that it is ready + mapSync.Lock() + proxyMap[ctxid] = true + mapSync.Unlock() + halt := false + for { + client, err := listener.Accept() + if err != nil { + SysLogger.Error().Err(err).Msgf("failed to accept connection at %d", ctxid) + continue + } + + // we pass the actual tcp connection + go handleTcpConnection(client, ctxid) + select { + // once the http session is gone we stop the listener + case <-ctx.Done(): + SysLogger.Debug().Msgf("stopping personal tcp forwarer at " + socketPath) + halt = true + } + if halt { + break + } + } + // once the http session is gone, the socket and the user and proxymaps are getting cleaned up + os.Remove(socketPath) + mapSync.Lock() + delete(proxyMap, ctxid) + delete(userMap, ctxid) + mapSync.Unlock() + + commandSync.Lock() + delete(commandMap, ctxid) + commandSync.Unlock() +} + +func handleTcpConnection(client net.Conn, ctxid string) { + // setting up the upstream connection + target, err := tls.Dial("tcp", targetAddress, &tls.Config{RootCAs: CAPool}) + if err != nil { + SysLogger.Error().Err(err).Msgf("failed to connect to upstream at %d", ctxid) + client.Close() + return + } + defer target.Close() + + // we are creating an instance of TCPLogger + // which implements net.conn and custom logging + // with the context of the user we are logging + // traffic for + tcpLogger := &TCPLogger{Conn: target, ctxid: ctxid} + + // on the way toward the target we send the traffic + // through the tcp logger + go io.Copy(tcpLogger, client) + // on the way back however we dont want to log anything + io.Copy(client, target) + client.Close() +} + +type TCPLogger struct { + net.Conn + ctxid string +} + +func (t *TCPLogger) Read(b []byte) (n int, err error) { + n, err = t.Conn.Read(b) + return +} + +func (t *TCPLogger) Write(b []byte) (n int, err error) { + n, err = t.Conn.Write(b) + if n > 0 { + // we need parse the websockter frame + frame, err := parseWebSocketFrame(b) + if err != nil { + SysLogger.Error().Err(err).Msg("failed to parse ws frame") + } + if frame != nil { + // if it is opscode 0x2 we log out + // activities + if frame.Opcode == 0x2 { + if auditLogger.GetLevel() == zerolog.TraceLevel { + stroke, err := hex.DecodeString(fmt.Sprintf("%x", frame.Payload)) + SysLogger.Error().Err(err).Msg("failed to parse payload") + + auditLogger.Trace().Str("user", userMap[t.ctxid]).Str("session", t.ctxid).Str("stroke", strings.ReplaceAll(string(stroke), "\u0000", "")).Msg("") + asyncAuditChan <- asyncAudit{ + ctxid: t.ctxid, + ascii: frame.Payload, + } + } + + } + } + } + return +}