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