From 6d024027de3177535dd96e28bf86f135d8775e6c Mon Sep 17 00:00:00 2001 From: n-marton Date: Tue, 17 Dec 2024 15:12:59 +0100 Subject: [PATCH] initial code Signed-off-by: n-marton --- .github/CODEOWNERS | 1 + .github/dependabot.yml | 7 + .github/workflows/build_latest_image.yml | 39 ++++ DESIGN.md | 7 + Dockerfile | 19 ++ GUIDE.md | 11 + LOGO.png | Bin 0 -> 16570 bytes README.md | 25 +++ STARTED.md | 19 ++ diagram.png | Bin 0 -> 99680 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 ++++++++++++ 28 files changed, 1483 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build_latest_image.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/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..0043e165190285f14800b324ad19e375eb8f8241 GIT binary patch literal 99680 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

{djAwh2^&P%6 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 Nf{coE-o1y<{|9$p=8pgX 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..f1b45ea --- /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: 2 + 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 +}