diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..7db07fc7 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,42 @@ +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '32 3 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..80f84b31 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,85 @@ +name: docker-nightly + +on: + push: + branches: + - main + tags: + - '*.*.*' + pull_request: + +jobs: + + docker: + name: Docker + runs-on: ubuntu-latest + + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.24.x" + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get Build Data + id: info + run: | + echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ') + export TEMP=$(cd auth_server && go run gen_version.go) + echo ::set-output name=version::$(echo -n $TEMP | awk '{print $1}') + echo ::set-output name=build_id::$(echo -n $TEMP | awk '{print $2}') + + - name: Docker meta + id: docker_meta + uses: crazy-max/ghaction-docker-meta@v5 + with: + images: cesanta/docker_auth + tag-edge: true + tag-semver: | + {{version}} + {{major}} + {{major}}.{{minor}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + with: + install: true + version: latest + # TODO: Remove driver-opts once fix is released docker/buildx#386 + driver-opts: image=moby/buildkit:master + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + if: github.event_name == 'push' + + - name: Build and Push + uses: docker/build-push-action@v6 + with: + context: auth_server + file: auth_server/Dockerfile + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.docker_meta.outputs.tags }} + build-args: | + VERSION=${{ steps.info.outputs.version }} + BUILD_ID=${{ steps.info.outputs.build_id }} + labels: | + org.opencontainers.image.title=${{ github.event.repository.name }} + org.opencontainers.image.description=${{ github.event.repository.description }} + org.opencontainers.image.url=${{ github.event.repository.html_url }} + org.opencontainers.image.source=${{ github.event.repository.clone_url }} + org.opencontainers.image.version=${{ steps.imagetag.outputs.value }} + org.opencontainers.image.created=${{ steps.info.outputs.created }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.licenses=${{ github.event.repository.license.spdx_id }} diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml new file mode 100644 index 00000000..50c4821b --- /dev/null +++ b/.github/workflows/go_test.yml @@ -0,0 +1,24 @@ +on: [push, pull_request] +name: Test +jobs: + test: + strategy: + matrix: + go-version: [1.23.x,1.24.x] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v4 + - name: Test + run: | + cd auth_server + go test ./... + - name: Build + run: | + cd auth_server + make diff --git a/.gitignore b/.gitignore index 1377554e..5aaadfcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.swp +chart/docker-auth/Chart.lock diff --git a/README.md b/README.md index dec6f38e..5e00a657 100644 --- a/README.md +++ b/README.md @@ -8,27 +8,37 @@ While performing simple user authentication is pretty straightforward, performin Docker Registry 2.0 introduced a new, token-based authentication and authorization protocol, but the server to generate them was not released. Thus, most guides found on the internet still describe a set up with a reverse proxy performing access control. -This server fills the gap and implements the protocol described [here](https://github.com/docker/distribution/blob/master/docs/spec/auth/token.md). +This server fills the gap and implements the protocol described [here](https://github.com/docker/distribution/blob/main/docs/spec/auth/token.md). Supported authentication methods: * Static list of users - * Google Sign-In (incl. Google for Work / GApps for domain) (documented [here](https://github.com/cesanta/docker_auth/blob/master/examples/reference.yml)) + * Google Sign-In (incl. Google for Work / GApps for domain) (documented [here](https://github.com/cesanta/docker_auth/blob/main/examples/reference.yml)) * [Github Sign-In](docs/auth-methods.md#github) + * Gitlab Sign-In * LDAP bind ([demo](https://github.com/kwk/docker-registry-setup)) * MongoDB user collection - * [External program](https://github.com/cesanta/docker_auth/blob/master/examples/ext_auth.sh) + * MySQL/MariaDB, PostgreSQL, SQLite database table + * [External program](https://github.com/cesanta/docker_auth/blob/main/examples/ext_auth.sh) Supported authorization methods: * Static ACL * MongoDB-backed ACL + * MySQL/MariaDB, PostgreSQL, SQLite backed ACL * External program ## Installation and Examples -A public Docker image is available on Docker Hub: [cesanta/docker_auth](https://registry.hub.docker.com/u/cesanta/docker_auth/). +### Using Helm/Kubernetes + +A helm chart is available in the folder [chart/docker-auth](chart/docker-auth). + +### Docker + +A public Docker image is available on Docker Hub: [cesanta/docker_auth](https://hub.docker.com/r/cesanta/docker_auth/). Tags available: - - `:latest` - bleeding edge, usually works but breaking config changes are possible. You probably do not want to use this in production. + - `:edge` - bleeding edge, usually works but breaking config changes are possible. You probably do not want to use this in production. + - `:latest` - latest tagged release, will line up with `:1` tag - `:1` - the `1.x` version, will have fixes, no breaking config changes. Previously known as `:stable`. - `:1.x` - specific release, see [here](https://github.com/cesanta/docker_auth/releases) for the list of current releases. @@ -45,7 +55,7 @@ $ docker run \ cesanta/docker_auth:1 /config/auth_config.yml ``` -See the [example config files](https://github.com/cesanta/docker_auth/tree/master/examples/) to get an idea of what is possible. +See the [example config files](https://github.com/cesanta/docker_auth/tree/main/examples/) to get an idea of what is possible. ## Troubleshooting diff --git a/auth_server/Dockerfile b/auth_server/Dockerfile index 898026a3..c489ad6e 100644 --- a/auth_server/Dockerfile +++ b/auth_server/Dockerfile @@ -1,6 +1,20 @@ -FROM busybox -ADD auth_server /docker_auth/ -COPY ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +FROM golang:1.24-alpine3.22 AS build + +ARG VERSION +ENV VERSION="${VERSION}" +ARG BUILD_ID +ENV BUILD_ID="${BUILD_ID}" +ARG CGO_EXTRA_CFLAGS + +RUN apk add -U --no-cache ca-certificates make git gcc musl-dev binutils-gold + +COPY . /build +WORKDIR /build +RUN make build + +FROM alpine:3.22 +COPY --from=build /build/auth_server /docker_auth/ +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ ENTRYPOINT ["/docker_auth/auth_server"] CMD ["/config/auth_config.yml"] EXPOSE 5001 diff --git a/auth_server/Makefile b/auth_server/Makefile index d1d136b9..120d1a89 100644 --- a/auth_server/Makefile +++ b/auth_server/Makefile @@ -1,42 +1,14 @@ MAKEFLAGS += --warn-undefined-variables IMAGE ?= cesanta/docker_auth -COMPRESS_BINARY ?= false -CA_BUNDLE = /etc/ssl/certs/ca-certificates.crt -VERSION = $(shell cat version.txt) - -BUILDER_IMAGE ?= golang:1.12.8-alpine +VERSION ?= $(shell go run ./gen_version.go | awk '{print $$1}') +BUILD_ID ?= $(shell go run ./gen_version.go | awk '{print $$2}') .PHONY: % all: build -deps: - go install -v github.com/a-urth/go-bindata/go-bindata - -generate: - go generate \ - github.com/cesanta/docker_auth/auth_server \ - github.com/cesanta/docker_auth/auth_server/authn/... \ - github.com/cesanta/docker_auth/auth_server/authz/... \ - github.com/cesanta/docker_auth/auth_server/mgo_session/... \ - github.com/cesanta/docker_auth/auth_server/server/... - build: - CGO_ENABLED=0 go build -v --ldflags=--s - -ca-certificates.crt: - cp $(CA_BUNDLE) . - -build-release: ca-certificates.crt - docker run --rm -v $(PWD)/..:/src \ - $(BUILDER_IMAGE) sh -x -c "\ - apk update && apk add git make py2-pip && pip install GitPython && \ - cd /src/auth_server && \ - umask 0 && \ - go install -v github.com/a-urth/go-bindata/go-bindata && \ - make generate && \ - CGO_ENABLED=0 go build -v --ldflags=--s" - @echo === Built version $$(cat version.txt) === + go build -v -ldflags="-extldflags '-static' -X 'main.Version=${VERSION}' -X 'main.BuildID=${BUILD_ID}'" auth_server: @echo @@ -45,7 +17,7 @@ auth_server: @exit 1 docker-build: - docker build -t $(IMAGE):latest . + docker build --build-arg VERSION="${VERSION}" --build-arg BUILD_ID="${BUILD_ID}" -t $(IMAGE):latest . docker tag $(IMAGE):latest $(IMAGE):$(VERSION) docker-tag-%: diff --git a/auth_server/README.md b/auth_server/README.md index 7911f999..00f30fe5 100644 --- a/auth_server/README.md +++ b/auth_server/README.md @@ -1,17 +1,9 @@ ### Building local image ``` -# copy ca certificate to /etc/ssl/certs/ca-certificates.crt -pip install gitpython mkdir -p /var/tmp/go/src/github.com/cesanta cd /var/tmp/go/src/github.com/cesanta git clone https://github.com/cesanta/docker_auth.git cd docker_auth/auth_server -export GOPATH=/var/tmp/go -export PATH=$PATH:$GOPATH/bin -# download dependencies -make deps -# build source -make generate -make +make docker-build ``` diff --git a/auth_server/api/authn.go b/auth_server/api/authn.go new file mode 100644 index 00000000..8cd132f8 --- /dev/null +++ b/auth_server/api/authn.go @@ -0,0 +1,52 @@ +/* + Copyright 2019 Cesanta Software Ltd. + + 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 + + https://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 api + +import "errors" + +type Labels map[string][]string + +// Authentication plugin interface. +type Authenticator interface { + // Given a user name and a password (plain text), responds with the result or an error. + // Error should only be reported if request could not be serviced, not if it should be denied. + // A special NoMatch error is returned if the authorizer could not reach a decision, + // e.g. none of the rules matched. + // Another special WrongPass error is returned if the authorizer failed to authenticate. + // Implementations must be goroutine-safe. + Authenticate(user string, password PasswordString) (bool, Labels, error) + + // Finalize resources in preparation for shutdown. + // When this call is made there are guaranteed to be no Authenticate requests in flight + // and there will be no more calls made to this instance. + Stop() + + // Human-readable name of the authenticator. + Name() string +} + +var NoMatch = errors.New("did not match any rule") +var WrongPass = errors.New("wrong password for user") + +type PasswordString string + +func (ps PasswordString) String() string { + if len(ps) == 0 { + return "" + } + return "***" +} diff --git a/auth_server/authz/authz.go b/auth_server/api/authz.go similarity index 69% rename from auth_server/authz/authz.go rename to auth_server/api/authz.go index 53eba0e0..6d03ead8 100644 --- a/auth_server/authz/authz.go +++ b/auth_server/api/authz.go @@ -1,12 +1,25 @@ -package authz +/* + Copyright 2019 Cesanta Software Ltd. + + 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 + + https://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 api import ( - "errors" "fmt" "net" "strings" - - "github.com/cesanta/docker_auth/auth_server/authn" ) // Authorizer interface performs authorization of the request. @@ -32,8 +45,6 @@ type Authorizer interface { Name() string } -var NoMatch = errors.New("did not match any rule") - type AuthRequestInfo struct { Account string Type string @@ -41,7 +52,7 @@ type AuthRequestInfo struct { Service string IP net.IP Actions []string - Labels authn.Labels + Labels Labels } func (ai AuthRequestInfo) String() string { diff --git a/auth_server/authn/authn.go b/auth_server/authn/authn.go index 70e56e82..a3ab2461 100644 --- a/auth_server/authn/authn.go +++ b/auth_server/authn/authn.go @@ -1,5 +1,5 @@ /* - Copyright 2015 Cesanta Software Ltd. + Copyright 2020 Cesanta Software Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,39 +16,7 @@ package authn -import "errors" +import "embed" -type Labels map[string][]string - -// Authentication plugin interface. -type Authenticator interface { - // Given a user name and a password (plain text), responds with the result or an error. - // Error should only be reported if request could not be serviced, not if it should be denied. - // A special NoMatch error is returned if the authorizer could not reach a decision, - // e.g. none of the rules matched. - // Another special WrongPass error is returned if the authorizer failed to authenticate. - // Implementations must be goroutine-safe. - Authenticate(user string, password PasswordString) (bool, Labels, error) - - // Finalize resources in preparation for shutdown. - // When this call is made there are guaranteed to be no Authenticate requests in flight - // and there will be no more calls made to this instance. - Stop() - - // Human-readable name of the authenticator. - Name() string -} - -var NoMatch = errors.New("did not match any rule") -var WrongPass = errors.New("wrong password for user") - -//go:generate go-bindata -pkg authn -modtime 1 -mode 420 -nocompress data/ - -type PasswordString string - -func (ps PasswordString) String() string { - if len(ps) == 0 { - return "" - } - return "***" -} +//go:embed data/* +var static embed.FS diff --git a/auth_server/authn/bindata.go b/auth_server/authn/bindata.go deleted file mode 100644 index a37fc695..00000000 --- a/auth_server/authn/bindata.go +++ /dev/null @@ -1,466 +0,0 @@ -// Code generated by go-bindata. -// sources: -// data/github_auth.tmpl -// data/github_auth_result.tmpl -// data/google_auth.tmpl -// DO NOT EDIT! - -package authn - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" -) -type asset struct { - bytes []byte - info os.FileInfo -} - -type bindataFileInfo struct { - name string - size int64 - mode os.FileMode - modTime time.Time -} - -func (fi bindataFileInfo) Name() string { - return fi.name -} -func (fi bindataFileInfo) Size() int64 { - return fi.size -} -func (fi bindataFileInfo) Mode() os.FileMode { - return fi.mode -} -func (fi bindataFileInfo) ModTime() time.Time { - return fi.modTime -} -func (fi bindataFileInfo) IsDir() bool { - return false -} -func (fi bindataFileInfo) Sys() interface{} { - return nil -} - -var _dataGithub_authTmpl = []byte(` - - -
- -
-
-
- Login{{if .Organization}} to @{{.Organization}}{{end}} with GitHub
-
-
- Revoke access -
-$ docker login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
-
-
-`)
-
-func dataGithub_auth_resultTmplBytes() ([]byte, error) {
- return _dataGithub_auth_resultTmpl, nil
-}
-
-func dataGithub_auth_resultTmpl() (*asset, error) {
- bytes, err := dataGithub_auth_resultTmplBytes()
- if err != nil {
- return nil, err
- }
-
- info := bindataFileInfo{name: "data/github_auth_result.tmpl", size: 1300, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
- a := &asset{bytes: bytes, info: info}
- return a, nil
-}
-
-var _dataGoogle_authTmpl = []byte(`
-
-
-
-
-
-
-
-
-
-
-
-
-
-`)
-
-func dataGoogle_authTmplBytes() ([]byte, error) {
- return _dataGoogle_authTmpl, nil
-}
-
-func dataGoogle_authTmpl() (*asset, error) {
- bytes, err := dataGoogle_authTmplBytes()
- if err != nil {
- return nil, err
- }
-
- info := bindataFileInfo{name: "data/google_auth.tmpl", size: 2817, mode: os.FileMode(420), modTime: time.Unix(1, 0)}
- a := &asset{bytes: bytes, info: info}
- return a, nil
-}
-
-// Asset loads and returns the asset for the given name.
-// It returns an error if the asset could not be found or
-// could not be loaded.
-func Asset(name string) ([]byte, error) {
- cannonicalName := strings.Replace(name, "\\", "/", -1)
- if f, ok := _bindata[cannonicalName]; ok {
- a, err := f()
- if err != nil {
- return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
- }
- return a.bytes, nil
- }
- return nil, fmt.Errorf("Asset %s not found", name)
-}
-
-// MustAsset is like Asset but panics when Asset would return an error.
-// It simplifies safe initialization of global variables.
-func MustAsset(name string) []byte {
- a, err := Asset(name)
- if err != nil {
- panic("asset: Asset(" + name + "): " + err.Error())
- }
-
- return a
-}
-
-// AssetInfo loads and returns the asset info for the given name.
-// It returns an error if the asset could not be found or
-// could not be loaded.
-func AssetInfo(name string) (os.FileInfo, error) {
- cannonicalName := strings.Replace(name, "\\", "/", -1)
- if f, ok := _bindata[cannonicalName]; ok {
- a, err := f()
- if err != nil {
- return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
- }
- return a.info, nil
- }
- return nil, fmt.Errorf("AssetInfo %s not found", name)
-}
-
-// AssetNames returns the names of the assets.
-func AssetNames() []string {
- names := make([]string, 0, len(_bindata))
- for name := range _bindata {
- names = append(names, name)
- }
- return names
-}
-
-// _bindata is a table, holding each asset generator, mapped to its name.
-var _bindata = map[string]func() (*asset, error){
- "data/github_auth.tmpl": dataGithub_authTmpl,
- "data/github_auth_result.tmpl": dataGithub_auth_resultTmpl,
- "data/google_auth.tmpl": dataGoogle_authTmpl,
-}
-
-// AssetDir returns the file names below a certain
-// directory embedded in the file by go-bindata.
-// For example if you run go-bindata on data/... and data contains the
-// following hierarchy:
-// data/
-// foo.txt
-// img/
-// a.png
-// b.png
-// then AssetDir("data") would return []string{"foo.txt", "img"}
-// AssetDir("data/img") would return []string{"a.png", "b.png"}
-// AssetDir("foo.txt") and AssetDir("notexist") would return an error
-// AssetDir("") will return []string{"data"}.
-func AssetDir(name string) ([]string, error) {
- node := _bintree
- if len(name) != 0 {
- cannonicalName := strings.Replace(name, "\\", "/", -1)
- pathList := strings.Split(cannonicalName, "/")
- for _, p := range pathList {
- node = node.Children[p]
- if node == nil {
- return nil, fmt.Errorf("Asset %s not found", name)
- }
- }
- }
- if node.Func != nil {
- return nil, fmt.Errorf("Asset %s not found", name)
- }
- rv := make([]string, 0, len(node.Children))
- for childName := range node.Children {
- rv = append(rv, childName)
- }
- return rv, nil
-}
-
-type bintree struct {
- Func func() (*asset, error)
- Children map[string]*bintree
-}
-var _bintree = &bintree{nil, map[string]*bintree{
- "data": &bintree{nil, map[string]*bintree{
- "github_auth.tmpl": &bintree{dataGithub_authTmpl, map[string]*bintree{}},
- "github_auth_result.tmpl": &bintree{dataGithub_auth_resultTmpl, map[string]*bintree{}},
- "google_auth.tmpl": &bintree{dataGoogle_authTmpl, map[string]*bintree{}},
- }},
-}}
-
-// RestoreAsset restores an asset under the given directory
-func RestoreAsset(dir, name string) error {
- data, err := Asset(name)
- if err != nil {
- return err
- }
- info, err := AssetInfo(name)
- if err != nil {
- return err
- }
- err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
- if err != nil {
- return err
- }
- err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
- if err != nil {
- return err
- }
- err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
- if err != nil {
- return err
- }
- return nil
-}
-
-// RestoreAssets restores an asset under the given directory recursively
-func RestoreAssets(dir, name string) error {
- children, err := AssetDir(name)
- // File
- if err != nil {
- return RestoreAsset(dir, name)
- }
- // Dir
- for _, child := range children {
- err = RestoreAssets(dir, filepath.Join(name, child))
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-func _filePath(dir, name string) string {
- cannonicalName := strings.Replace(name, "\\", "/", -1)
- return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
-}
-
diff --git a/auth_server/authn/data/github_auth_result.tmpl b/auth_server/authn/data/github_auth_result.tmpl
index 2619d0cd..513034ea 100644
--- a/auth_server/authn/data/github_auth_result.tmpl
+++ b/auth_server/authn/data/github_auth_result.tmpl
@@ -43,9 +43,11 @@
$ docker login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+ $ podman login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}
+ $ nerdctl login -u {{.Username}} -p {{.Password}} {{if .RegistryUrl}}{{.RegistryUrl}}{{else}}docker.example.com{{end}}