From 91ceae23d0885de9a452afc692db6d5cb751b40f Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Mon, 10 Jun 2019 07:57:42 -0700 Subject: [PATCH] Add support for customizable user (#7569) --- .gitignore | 1 + Dockerfile | 4 +- Dockerfile.dev | 7 +- Dockerfile.release | 9 +- Makefile | 1 + cmd/endpoint-ellipses.go | 10 +-- cmd/gateway-main.go | 4 +- cmd/main.go | 6 +- cmd/server-main.go | 5 +- dockerscripts/check-user.go | 136 +++++++++++++++++++++++++++++ dockerscripts/docker-entrypoint.sh | 48 ++++++++-- dockerscripts/healthcheck.go | 15 +++- docs/docker/README.md | 29 ++++++ 13 files changed, 244 insertions(+), 31 deletions(-) create mode 100644 dockerscripts/check-user.go diff --git a/.gitignore b/.gitignore index db1163546..631ffd7da 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ stage/ .sia_temp/ config.json healthcheck +check-user diff --git a/Dockerfile b/Dockerfile index 4ce2cf272..7cb913b3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,8 @@ RUN \ apk add --no-cache git && \ git clone https://github.com/minio/minio && cd minio && \ go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \ - cd dockerscripts; go build -ldflags "-s -w" -o /usr/bin/healthcheck healthcheck.go + cd dockerscripts; go build -tags kqueue -ldflags "-s -w" -o /usr/bin/healthcheck healthcheck.go && \ + go build -tags kqueue -ldflags "-s -w" -o /usr/bin/check-user check-user.go FROM alpine:3.9 @@ -22,6 +23,7 @@ EXPOSE 9000 COPY --from=0 /go/bin/minio /usr/bin/minio COPY --from=0 /usr/bin/healthcheck /usr/bin/healthcheck +COPY --from=0 /usr/bin/check-user /usr/bin/check-user COPY dockerscripts/docker-entrypoint.sh /usr/bin/ RUN \ diff --git a/Dockerfile.dev b/Dockerfile.dev index a2fabcade..6e7de93b3 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -2,7 +2,7 @@ FROM alpine:3.9 LABEL maintainer="MinIO Inc " -COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck /usr/bin/ +COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck dockerscripts/check-user /usr/bin/ COPY minio /usr/bin/ ENV MINIO_UPDATE off @@ -10,11 +10,12 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \ MINIO_SECRET_KEY_FILE=secret_key RUN \ - apk add --no-cache ca-certificates 'curl>7.61.0' && \ + apk add --no-cache ca-certificates 'curl>7.61.0' 'su-exec>=0.2' && \ echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \ chmod +x /usr/bin/minio && \ chmod +x /usr/bin/docker-entrypoint.sh && \ - chmod +x /usr/bin/healthcheck + chmod +x /usr/bin/healthcheck && \ + chmod +x /usr/bin/check-user EXPOSE 9000 diff --git a/Dockerfile.release b/Dockerfile.release index f287860a3..9f70ef4c0 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -7,13 +7,15 @@ ENV GO111MODULE on RUN \ apk add --no-cache git && \ git clone https://github.com/minio/minio && cd minio/dockerscripts && \ - go build -ldflags "-s -w" -o /usr/bin/healthcheck healthcheck.go + go build -tags kqueue -ldflags "-s -w" -o /usr/bin/healthcheck healthcheck.go && \ + go build -tags kqueue -ldflags "-s -w" -o /usr/bin/check-user check-user.go FROM alpine:3.9 LABEL maintainer="MinIO Inc " COPY --from=0 /usr/bin/healthcheck /usr/bin/healthcheck +COPY --from=0 /usr/bin/check-user /usr/bin/check-user COPY dockerscripts/docker-entrypoint.sh /usr/bin/ ENV MINIO_UPDATE off @@ -21,12 +23,13 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \ MINIO_SECRET_KEY_FILE=secret_key RUN \ - apk add --no-cache ca-certificates 'curl>7.61.0' && \ + apk add --no-cache ca-certificates 'curl>7.61.0' 'su-exec>=0.2' && \ echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \ curl https://dl.min.io/server/minio/release/linux-amd64/minio > /usr/bin/minio && \ chmod +x /usr/bin/minio && \ chmod +x /usr/bin/docker-entrypoint.sh && \ - chmod +x /usr/bin/healthcheck + chmod +x /usr/bin/healthcheck && \ + chmod +x /usr/bin/check-user EXPOSE 9000 diff --git a/Makefile b/Makefile index 1972c69ed..efdaa6167 100644 --- a/Makefile +++ b/Makefile @@ -73,6 +73,7 @@ build: checks @echo "Building minio binary to './minio'" @GO111MODULE=on GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null @GO111MODULE=on GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/dockerscripts/healthcheck $(PWD)/dockerscripts/healthcheck.go 1>/dev/null + @GO111MODULE=on GOFLAGS="" CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/dockerscripts/check-user $(PWD)/dockerscripts/check-user.go 1>/dev/null docker: build @docker build -t $(TAG) . -f Dockerfile.dev diff --git a/cmd/endpoint-ellipses.go b/cmd/endpoint-ellipses.go index 654533c22..d456f98ba 100644 --- a/cmd/endpoint-ellipses.go +++ b/cmd/endpoint-ellipses.go @@ -213,12 +213,12 @@ func parseEndpointSet(args ...string) (ep endpointSet, err error) { return ep, nil } -// Parses all ellipses input arguments, expands them into corresponding -// list of endpoints chunked evenly in accordance with a specific -// set size. +// GetAllSets - parses all ellipses input arguments, expands them into +// corresponding list of endpoints chunked evenly in accordance with a +// specific set size. // For example: {1...64} is divided into 4 sets each of size 16. // This applies to even distributed setup syntax as well. -func getAllSets(args ...string) ([][]string, error) { +func GetAllSets(args ...string) ([][]string, error) { if len(args) == 0 { return nil, errInvalidArgument } @@ -266,7 +266,7 @@ func getAllSets(args ...string) ([][]string, error) { // CreateServerEndpoints - validates and creates new endpoints from input args, supports // both ellipses and without ellipses transparently. func createServerEndpoints(serverAddr string, args ...string) (string, EndpointList, SetupType, int, int, error) { - setArgs, err := getAllSets(args...) + setArgs, err := GetAllSets(args...) if err != nil { return serverAddr, nil, -1, 0, 0, err } diff --git a/cmd/gateway-main.go b/cmd/gateway-main.go index 4937198c5..90f0a0e1c 100644 --- a/cmd/gateway-main.go +++ b/cmd/gateway-main.go @@ -41,14 +41,14 @@ var ( gatewayCmd = cli.Command{ Name: "gateway", Usage: "start object storage gateway", - Flags: append(serverFlags, globalFlags...), + Flags: append(ServerFlags, GlobalFlags...), HideHelpCommand: true, } ) // RegisterGatewayCommand registers a new command for gateway. func RegisterGatewayCommand(cmd cli.Command) error { - cmd.Flags = append(append(cmd.Flags, append(cmd.Flags, serverFlags...)...), globalFlags...) + cmd.Flags = append(append(cmd.Flags, append(cmd.Flags, ServerFlags...)...), GlobalFlags...) gatewayCmd.Subcommands = append(gatewayCmd.Subcommands, cmd) return nil } diff --git a/cmd/main.go b/cmd/main.go index 6bf7a64d6..fb92a7687 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -27,8 +27,8 @@ import ( "github.com/minio/minio/pkg/words" ) -// global flags for minio. -var globalFlags = []cli.Flag{ +// GlobalFlags - global flags for minio. +var GlobalFlags = []cli.Flag{ cli.StringFlag{ Name: "config-dir, C", Value: defaultConfigDir.Get(), @@ -131,7 +131,7 @@ func newApp(name string) *cli.App { app.Version = Version app.Usage = "Cloud Storage Server." app.Description = `MinIO is an Amazon S3 compatible object storage server. Use it to store photos, videos, VMs, containers, log files, or any blob of data as objects.` - app.Flags = globalFlags + app.Flags = GlobalFlags app.HideVersion = true // Hide `--version` flag, we already have `minio version`. app.HideHelpCommand = true // Hide `help, h` command, we already have `minio --help`. app.Commands = commands diff --git a/cmd/server-main.go b/cmd/server-main.go index 6cf953e50..6409a7c22 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -38,7 +38,8 @@ func init() { logger.RegisterUIError(fmtError) } -var serverFlags = []cli.Flag{ +// ServerFlags - server command specific flags +var ServerFlags = []cli.Flag{ cli.StringFlag{ Name: "address", Value: ":" + globalMinioDefaultPort, @@ -49,7 +50,7 @@ var serverFlags = []cli.Flag{ var serverCmd = cli.Command{ Name: "server", Usage: "start object storage server", - Flags: append(serverFlags, globalFlags...), + Flags: append(ServerFlags, GlobalFlags...), Action: serverMain, CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} diff --git a/dockerscripts/check-user.go b/dockerscripts/check-user.go new file mode 100644 index 000000000..00fe03f8a --- /dev/null +++ b/dockerscripts/check-user.go @@ -0,0 +1,136 @@ +// +build ignore + +/* + * MinIO Cloud Storage, (C) 2019 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "os/user" + "syscall" + + "github.com/minio/cli" + minio "github.com/minio/minio/cmd" +) + +var defaultUserGroup string + +func init() { + username := os.Getenv("MINIO_USERNAME") + groupname := os.Getenv("MINIO_GROUPNAME") + defaultUserGroup = username + ":" + groupname +} + +func getUserGroup(path string) (string, error) { + fi, err := os.Stat(minio.PathJoin(path, ".minio.sys")) + if err != nil { + // Fresh directory we should default to what was requested by user. + if os.IsNotExist(err) { + cmd := exec.Command("chown", "-R", defaultUserGroup, path) + if err = cmd.Run(); err != nil { + return "", err + } + return defaultUserGroup, nil + } + return "", err + } + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + // Unable to figure out uid/gid, default to defaultUserGroup + return defaultUserGroup, nil + } + u, err := user.LookupId(fmt.Sprintf("%d", stat.Uid)) + if err != nil { + return fmt.Sprintf("%d:%d", stat.Uid, stat.Gid), nil + } + g, err := user.LookupGroupId(fmt.Sprintf("%d", stat.Gid)) + if err != nil { + return fmt.Sprintf("%d:%d", stat.Uid, stat.Gid), nil + } + return fmt.Sprintf("%s:%s", u.Username, g.Name), nil +} + +func main() { + app := cli.NewApp() + app.Flags = append(minio.ServerFlags, minio.GlobalFlags...) + app.Action = func(ctx *cli.Context) { + // Fetch address option + serverAddr := ctx.GlobalString("address") + if serverAddr == "" || serverAddr == ":9000" { + serverAddr = ctx.String("address") + } + if ctx.Args().First() == "help" { + cli.ShowCommandHelpAndExit(ctx, "check-user", 1) + } + if ctx.Args().First() != "minio" { + cli.ShowCommandHelpAndExit(ctx, "check-user", 1) + } + args := cli.Args(ctx.Args().Tail()) + if !args.Present() { + cli.ShowCommandHelpAndExit(ctx, "check-user", 1) + } + var ug string + var err error + switch args.First() { + case "gateway": + args = cli.Args(args.Tail()) + if args.First() != "nas" { + fmt.Println(defaultUserGroup) + return + } + args = cli.Args(args.Tail()) + if args.First() == "" { + fmt.Println("") + return + } + ug, err = getUserGroup(args.First()) + if err != nil { + log.Fatalln(err) + } + case "server": + var setArgs [][]string + setArgs, err = minio.GetAllSets(args.Tail()...) + if err != nil { + log.Fatalln(err) + } + var endpoints minio.EndpointList + _, endpoints, _, err = minio.CreateEndpoints(serverAddr, setArgs...) + if err != nil { + log.Fatalln(err) + } + for _, endpoint := range endpoints { + if !endpoint.IsLocal { + continue + } + ug, err = getUserGroup(endpoint.Path) + if err != nil { + log.Fatalln(err) + } + break + } + default: + cli.ShowCommandHelpAndExit(ctx, "check-user", 1) + } + fmt.Println(ug) + } + if err := app.Run(os.Args); err != nil { + log.Fatalln(err) + } +} diff --git a/dockerscripts/docker-entrypoint.sh b/dockerscripts/docker-entrypoint.sh index 57b8e78f9..8ac5d25ac 100755 --- a/dockerscripts/docker-entrypoint.sh +++ b/dockerscripts/docker-entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/sh # -# MinIO Cloud Storage, (C) 2017 MinIO, Inc. +# MinIO Cloud Storage, (C) 2019 MinIO, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ # limitations under the License. # +export MINIO_USERNAME=${MINIO_USERNAME:-"minio"} +export MINIO_GROUPNAME=${MINIO_GROUPNAME:-"minio"} + # If command starts with an option, prepend minio. if [ "${1}" != "minio" ]; then if [ -n "${1}" ]; then @@ -24,20 +27,47 @@ fi ## Look for docker secrets in default documented location. docker_secrets_env() { - local ACCESS_KEY_FILE="/run/secrets/$MINIO_ACCESS_KEY_FILE" - local SECRET_KEY_FILE="/run/secrets/$MINIO_SECRET_KEY_FILE" + ACCESS_KEY_FILE="/run/secrets/$MINIO_ACCESS_KEY_FILE" + SECRET_KEY_FILE="/run/secrets/$MINIO_SECRET_KEY_FILE" - if [ -f $ACCESS_KEY_FILE -a -f $SECRET_KEY_FILE ]; then - if [ -f $ACCESS_KEY_FILE ]; then - export MINIO_ACCESS_KEY="$(cat "$ACCESS_KEY_FILE")" + if [ -f "$ACCESS_KEY_FILE" ] && [ -f "$SECRET_KEY_FILE" ]; then + if [ -f "$ACCESS_KEY_FILE" ]; then + MINIO_ACCESS_KEY="$(cat "$ACCESS_KEY_FILE")" + export MINIO_ACCESS_KEY + fi + if [ -f "$SECRET_KEY_FILE" ]; then + MINIO_SECRET_KEY="$(cat "$SECRET_KEY_FILE")" + export MINIO_SECRET_KEY fi - if [ -f $SECRET_KEY_FILE ]; then - export MINIO_SECRET_KEY="$(cat "$SECRET_KEY_FILE")" + fi +} + +## Create UID/GID based on available environment variables. +docker_set_uid_gid() { + addgroup -S "$MINIO_GROUPNAME" >/dev/null 2>&1 && \ + adduser -S -G "$MINIO_GROUPNAME" "$MINIO_USERNAME" >/dev/null 2>&1 +} + +# su-exec to requested user, if user cannot be requested +# existing user is used automatically. +docker_switch_user() { + owner=$(check-user "$@") + if [ "${owner}" != "${MINIO_USERNAME}:${MINIO_GROUPNAME}" ]; then + ## Print the message only if we are not using non-default username:groupname. + if [ "${MINIO_USERNAME}:${MINIO_GROUPNAME}" != "minio:minio" ]; then + echo "Requested username/group ${MINIO_USERNAME}:${MINIO_GROUPNAME} cannot be used" + echo "Found existing data with user ${owner}, we will continue and use ${owner} instead." + return fi fi + exec su-exec "${owner}" "$@" } ## Set access env from secrets if necessary. docker_secrets_env -exec "$@" +## User Input UID and GID +docker_set_uid_gid + +## Switch to user if applicable. +docker_switch_user "$@" diff --git a/dockerscripts/healthcheck.go b/dockerscripts/healthcheck.go index 019215dde..02d471062 100755 --- a/dockerscripts/healthcheck.go +++ b/dockerscripts/healthcheck.go @@ -1,3 +1,5 @@ +// +build ignore + /* * MinIO Cloud Storage, (C) 2019 MinIO, Inc. * @@ -36,7 +38,7 @@ const ( initGraceTime = 300 healthPath = "/minio/health/live" timeout = time.Duration(30 * time.Second) - minioProcess = "minio" + tcp = "tcp" ) // returns container boot time by finding @@ -66,9 +68,16 @@ func findEndpoint() (string, error) { // split netstat output in rows scanner := bufio.NewScanner(stdout) scanner.Split(bufio.ScanLines) - // loop over the rows to find MinIO process + // MinIO works on TCP and it is supposed to be + // the only process listening on a port inside + // container. So we take the first row of netstat + // output (that has tcp) and assume that is the + // MinIO server port. + // Since MinIO is running as non-root user, we can + // no longer depend on the PID/Program name column + // of netstat output for scanner.Scan() { - if strings.Contains(scanner.Text(), minioProcess) { + if strings.Contains(scanner.Text(), tcp) { line := scanner.Text() newLine := strings.Replace(line, ":::", "127.0.0.1:", 1) fields := strings.Fields(newLine) diff --git a/docs/docker/README.md b/docs/docker/README.md index 116d94021..639963c46 100644 --- a/docs/docker/README.md +++ b/docs/docker/README.md @@ -58,6 +58,35 @@ docker run -p 9000:9000 --name minio1 \ minio/minio server /data ``` +### Run MinIO Docker as non root user +MinIO server runs as non-root within the container by default. However, this is applicable only if you're deploying new MinIO instance (not upgrading from older releases). Deployments upgrading from older MinIO deployments, will continue to run as the user previously used if any. + +By default `minio` is username and groupname. Use environment variables `MINIO_USERNAME` and `MINIO_GROUPNAME` to override these default values. + +#### GNU/Linux and macOS +```sh +docker run -p 9000:9000 --name minio1 \ + -e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \ + -e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \ + -e "MINIO_USERNAME=custom" \ + -e "MINIO_GROUPNAME=custom" \ + -v /mnt/data:/data \ + -v /mnt/config:/root/.minio \ + minio/minio server /data +``` + +#### Windows +```powershell +docker run -p 9000:9000 --name minio1 \ + -e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \ + -e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \ + -e "MINIO_USERNAME=custom" \ + -e "MINIO_GROUPNAME=custom" \ + -v D:\data:/data \ + -v D:\minio\config:/root/.minio \ + minio/minio server /data +``` + ### MinIO Custom Access and Secret Keys using Docker secrets To override MinIO's auto-generated keys, you may pass secret and access keys explicitly by creating access and secret keys as [Docker secrets](https://docs.docker.com/engine/swarm/secrets/). MinIO server also allows regular strings as access and secret keys.