aws / aws-sdk

Landing page for the AWS SDKs on GitHub
https://aws.amazon.com/tools/
Other
72 stars 14 forks source link

credential_process requires an available shell #152

Open anuraaga opened 2 years ago

anuraaga commented 2 years ago

Confirm by changing [ ] to [x] below to ensure that it's a bug:

Describe the bug credential_process always executes commands in a shell

https://github.com/aws/aws-sdk-go/blob/main/aws/credentials/processcreds/provider.go#L301

This prevents it from being usable in Docker images that don't have a shell such as scratch or distroless. There doesn't seem to be any reason that it shouldn't be possible to invoke a binary directly through credential_process without a shell in such environments, for statically compiled Go binaries, those base images are fairly popular now.

Omitting remaining sections since issue is obvious from the code.

anuraaga commented 2 years ago

/cc @xibz

KaibaLopez commented 2 years ago

Hi @anuraaga , Yeah, I see the use case.. though I might argue this is a feature request to support shell-less environments. Can you also just let me know if you've tried any workarounds?

anuraaga commented 2 years ago

@KaibaLopez I'm not aware of any workaround other than installing a shell into the docker image but many projects may not be able to do this, so we lose the ability to use credential process.

nicolasff commented 2 years ago

I'm not aware of any workaround other than installing a shell into the docker image

Hello! I work with @xibz (who opened the issue in aws-otel-collector) and one workaround we've found was to mount a copy of /bin/sh and its single dependency /lib/ld-musl-x86_64.so.1 from an Alpine image into the container. So yes, installing a shell into the docker image.

Something like:

mkdir -p ./alpine-shell/bin ./alpine-shell/lib
docker run --rm -ti alpine:3.15.0 base64 /bin/sh | base64 -D > ./alpine-shell/bin/sh
docker run --rm -ti alpine:3.15.0 base64 /lib/ld-musl-x86_64.so.1 | base64 -D > ./alpine-shell/lib/ld-musl-x86_64.so.1

and then run the container that uses the AWS SDK with -v ./alpine-shell/bin:/bin -v ./alpine-shell/lib:/lib

Needless to say we're unhappy with this (gross) workaround, but it does work for now as an intermediary between the Go program using the AWS SDK and our credential_process binary. Of course this isn't an option if the container needs any of the files it has under /bin or /lib.

Alternatively you could also write a statically linked program – in Go, C, or Rust maybe – that recognizes -c <command> and runs that command, or strips the initial -c and passes its parameters to a hardcoded credential provider.

Both approaches feel super hacky, more like "a workaround is possible if you really want it" rather than "here's a great way to resolve the issue". Just running the credentials provider directly seems like the better long-term fix.

rittneje commented 2 years ago

@KaibaLopez I would also point out even other SDKs disagree on this point.

Ruby and Python (and thus by extension the CLI) do not use the shell. https://github.com/aws/aws-sdk-ruby/blob/a82c8981c95a8296ffb6269c3c06a4f551d87f7d/gems/aws-sdk-core/lib/aws-sdk-core/process_credentials.rb#L36-L41 https://github.com/boto/botocore/blob/04d1fae43b657952e49b21d16daa86378ddb4253/botocore/credentials.py#L979-L984

However, C# does. https://github.com/aws/aws-sdk-net/blob/475822dec5e87954b7a47ac65995714ae1f1b115/sdk/src/Core/Amazon.Runtime/Credentials/ProcessAWSCredentials.cs#L58-L78

One interesting implication is that Go will ultimately perform environment variable expansion, while other languages will not.

package main

import (
    "fmt"
    "os"

    "github.com/aws/aws-sdk-go/aws/credentials/processcreds"
)

func main() {
    creds := processcreds.NewCredentials(`echo '{"Version":1,"AccessKeyId":"'"${FOO}"'","SecretAccessKey":"'"${BAR}"'"}'`)

    os.Setenv("FOO", "foo")
    os.Setenv("BAR", "bar")

    got, err := creds.Get()
    if err != nil {
        panic(err)
    }

    // {AccessKeyID:foo SecretAccessKey:bar SessionToken: ProviderName:ProcessProvider}
    fmt.Printf("%+v\n", got)
}

The actual documentation for credential_process makes no clear mention of a shell one way or the other. https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html

I would say that:

  1. The documentation should be clarified that no implicit shell will be spawned, and thus no environment variables will be expanded by default.
  2. The Go and C# SDKs should be fixed to align with the other SDKs. (For backwards compatibility this may need to be opt-in.)
  3. Users who actually want a shell should explicitly invoke /bin/sh (or similar) as the credential process.
skmcgrail commented 2 years ago

I don't believe this issue can be easily resolved, as arguably there is not limited to just Go and C#, Java, Javascript, PHP, Ruby, and C++ also rely on the shell for argument parsing and the respective environment. Any change of behavior would need to be done across all the SDKs to standardize on one set of behavior, and provide a mechanism which would designate or opt-in to the desired behavior, whatever that is determined to be. This issue is would likely be better driven by being sent to the aws-sdk tracking repository of issues as this is a cross-cutting behavior that we likely can't change in a vacuum.

Edit: Updated with more findings.

anuraaga commented 2 years ago

@skmcgrail Would you be able to transfer the issue to aws-sdk and help drive it there? One thing to keep in mind though is that it's far more common to not have a shell, e.g. in docker, for a go binary than any of those other languages thanks to static compilation so it's a higher priority for this language than the others possibly.

hans-d commented 1 year ago

see also: https://github.com/aws/aws-sdk-net/issues/1845 In the case of a locked down windows environment (no cmd.exe allowed for normal users)

jgrigg commented 5 months ago

Crazy hacky idea. Would publishing some public images where /bin/sh is actually not really a proper shell but a binary that runs the credential process (maybe written in go so we can target all platforms) and possibly takes some parameters. These would just be the base distroless images with this one extra file copied in and avoid an RCE attack.

Pretty gross but if it gets traction it might incentivise AWS to sort out the underlying issue.

Another option we're looking into is dropping a sidecar in our pods (we're in eks land) and asking for creds over http over the pod localhost. https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html

Would love to hear if anyone has had any luck with this. The pod identities docs don't mention this specifically but this guide mentions pod identities 🤔

I expect that pod identities being relatively new as opposed to irsa means their docs are in need of some tlc

pschulten commented 5 months ago

@jgrigg I had the exact same hack idea several years ago as well. It works flawlessly ever since. The golang repo produces two binaries.

  1. A shell "wrapper"

    func main() {
    fmt.Fprintf(os.Stderr, "DEBUG: sh wrapper args %s\n", os.Args)
    
    if os.Args[0] != "sh" || os.Args[1] != "-c" {
        panic("unexpected invocation. Expected sh -c *program*")
    }
    
    // credential_process code calls "sh -c (custom command)", we are looking for os.Args[2]
    fmt.Fprintf(os.Stderr, "DEBUG: execing %s\n", os.Args[2])
    command := strings.Split(os.Args[2], " ")
    binary, lookErr := exec.LookPath(command[0])
    if lookErr != nil {
        panic(lookErr)
    }
    
    env := os.Environ()
    
    execErr := syscall.Exec(binary, command, env)
    if execErr != nil {
        panic(execErr)
    }
    }
  2. the credential_process (In our case) using hashicorp vault call vark

it runs a sidecar in our clusters using this image:

FROM golang:1.22 as builder
ENV CGO_ENABLED=0
WORKDIR /workspace
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN go build -ldflags "-s -w" ./cmd/sh-wrapper && \
  go build -ldflags "-s -w" ./cmd/vark

FROM gcr.io/distroless/static:nonroot
ENV HOME=/home/nonroot
ENV AWS_SDK_LOAD_CONFIG=1
ENV AWS_SHARED_CREDENTIALS_FILE=/home/nonroot/.aws/credentials
ENV PATH="/"

COPY credentials /home/nonroot/.aws/credentials
COPY --from=builder /workspace/vark /vark
COPY --from=builder /workspace/sh-wrapper /sh

ENTRYPOINT ["/vark"]