From f314afdfb3fd5eeedb0854bffdaa14f0fc9401b0 Mon Sep 17 00:00:00 2001 From: n-marton Date: Tue, 17 Dec 2024 15:13:30 +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..1722c031eb231683b1228b32608bd8de6842362c GIT binary patch literal 99680 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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..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 +}