├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── README.md ├── Vagrantfile ├── api ├── handler.go └── handler_test.go ├── auth ├── basic.go ├── htpasswd.go ├── md5crypt.go ├── md5crypt_test.go ├── request.go ├── validate.go └── wrappers.go ├── commands └── garita.go ├── docker └── images │ ├── docker │ └── Dockerfile │ └── registry │ └── Dockerfile ├── main.go ├── token ├── jwt_token.go ├── jwt_token_test.go ├── scope.go └── scope_test.go ├── utils └── utils.go └── vagrant └── conf ├── ca_bundle ├── README.md ├── ca.crt ├── ca.key ├── server.crt ├── server.csr └── server.key ├── htpasswd └── registry-config.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | *.test 3 | garita 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | LineLength: 2 | Max: 100 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://travis-ci.org/dmacvicar/garita.svg?branch=master)](https://travis-ci.org/dmacvicar/garita) 3 | 4 | # Garita 5 | 6 | Small Docker v2 registry auth server in Go. 7 | 8 | It exists mostly as a project to learn Go, the Vagrant Docker provider, and 9 | to understand the protocol [Portus](https://github.com/SUSE/Portus) implements. 10 | 11 | ## Features 12 | 13 | * Authentication is only supported using htpasswd files 14 | * Once authenticated, it provides push and pull access to the 15 | /$user namespace 16 | 17 | Garita is inspired in [Portus](https://github.com/SUSE/Portus), which 18 | is a full featured auth server and registry index. 19 | 20 | ## Running 21 | 22 | Garita uses HTTPS by default. If you want to run over plain http (eg. for development purposes) you need to pass the option -http. Then you don't need to supply --tlscert and --tlskey options. 23 | 24 | ``` 25 | garita --key path/to/server.key --htpasswd path/to/htpasswd --tlskey path/to/server.key --tlscert path/to/server.crt 26 | ``` 27 | 28 | You can pass a configuration file in toml format with -c or --config. Any other configuration from the command line overrides the configuration file. 29 | 30 | At the same time you need to configure the registry 31 | 32 | ``` 33 | auth: 34 | token: 35 | realm: https://garita.yourdomain.com/v2/token 36 | service: registry.yourdomain.com 37 | issuer: garita.yourdomain.com 38 | rootcertbundle: /path/to/server.crt 39 | ``` 40 | 41 | If you use a self signed certificate, add the CA certificate to the system trusted anchors on the docker daemon host or add the certificate to: 42 | 43 | ``` 44 | /etc/docker/certs.d//ca.crt 45 | ``` 46 | 47 | ## Development Environment 48 | 49 | The environment creates 3 containers: 50 | 51 | * a Docker daemon (dockerd, dockerd.test.lan) 52 | * a Registry (registry, registry.test.lan) 53 | * garita (garita, garita.test.lan) 54 | 55 | While the images are based on opensuse:13.2, the dockerd container requires a host kernel 56 | with overlayfs support. (eg. openSUSE Tumbleweed or another distribution supporting 57 | overlayfs). The dockerd container is already privileged but I don't want to mess with the loop 58 | devices of the host. 59 | 60 | ## Running 61 | 62 | * Compile 63 | 64 | ``` 65 | go install github.com/dmacvicar/garita 66 | ``` 67 | 68 | * Start the environment 69 | 70 | ``` 71 | vagrant up --no-parallel 72 | ``` 73 | 74 | * Everytime you rebuild 75 | 76 | ``` 77 | vagrant reload garita 78 | ``` 79 | 80 | * To see the logs 81 | 82 | ``` 83 | vagrant docker-logs -f garita 84 | ``` 85 | 86 | Run docker against the docker daemon running inside the container 87 | 88 | ``` 89 | docker -H tcp://localhost:23750 images 90 | ``` 91 | 92 | The typical testcase, pull busybox, tag it, and push it to the registry 93 | 94 | ``` 95 | docker -H tcp://localhost:23750 pull busybox 96 | docker -H tcp://localhost:23750 tag busybox registry.test.lan/duncan/busybox 97 | docker login registry.test.lan 98 | docker -H tcp://localhost:23750 push registry.test.lan/duncan/busybox 99 | ``` 100 | 101 | # Bugs 102 | 103 | The [specification](https://docs.docker.com/registry/spec/auth/token/) does not go into every detail. If I missed something please open an issue. 104 | 105 | # Authors 106 | 107 | * Duncan-Mac-Vicar P. 108 | 109 | # License 110 | 111 | * Garita is licensed under the Apache 2.0 license. 112 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | Vagrant.require_version '>= 1.6.0' 4 | VAGRANTFILE_API_VERSION = '2' 5 | ENV['VAGRANT_DEFAULT_PROVIDER'] = 'docker' 6 | DOMAIN = 'test.lan' 7 | 8 | # Create and configure the Docker container(s) 9 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 10 | 11 | config.vm.define 'garita' do |container| 12 | container.vm.provider 'docker' do |d| 13 | d.image = 'opensuse:13.2' 14 | d.volumes = [File.join(Dir.pwd, 'garita') + ':/usr/bin/garita'] 15 | d.cmd = ['/usr/bin/garita', 16 | '--htpasswd', '/vagrant/vagrant/conf/htpasswd', 17 | '--key', '/vagrant/vagrant/conf/ca_bundle/server.key', 18 | '--tlscert', 19 | '/vagrant/vagrant/conf/ca_bundle/server.crt', 20 | '--tlskey', 21 | '/vagrant/vagrant/conf/ca_bundle/server.key' 22 | ] 23 | d.name = 'garita' 24 | d.create_args = ['-h', d.name + ".#{DOMAIN}", '--dns-search', DOMAIN] 25 | d.expose = [80] 26 | end 27 | end 28 | 29 | config.vm.define 'registry' do |container| 30 | container.vm.provider 'docker' do |d| 31 | d.build_dir = './docker/images/registry' 32 | d.name = 'registry' 33 | d.volumes = 34 | [File.expand_path('vagrant/conf/registry-config.yml') + 35 | ':/etc/registry-config.yml'] 36 | d.create_args = ['-h', d.name + ".#{DOMAIN}", '--dns-search', DOMAIN] 37 | d.link 'garita:garita.test.lan' 38 | d.expose = [80] 39 | end 40 | end 41 | 42 | config.vm.define 'dockerd' do |container| 43 | container.vm.provider 'docker' do |d| 44 | d.build_dir = './docker/images/docker' 45 | d.privileged = true 46 | d.name = 'dockerd' 47 | d.create_args = ['-h', d.name + ".#{DOMAIN}", '--dns-search', DOMAIN] 48 | d.ports = ['23750:2375'] 49 | d.volumes = 50 | [File.expand_path('vagrant/conf/ca_bundle/ca.crt') + 51 | ':/etc/docker/certs.d/garita.test.lan/ca.crt'] 52 | d.link 'registry:registry.test.lan' 53 | d.link 'garita:garita.test.lan' 54 | end 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /api/handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // package main 16 | // 17 | package api 18 | 19 | import ( 20 | "encoding/json" 21 | "github.com/dmacvicar/garita/auth" 22 | "github.com/dmacvicar/garita/token" 23 | "github.com/dmacvicar/garita/utils" 24 | "github.com/gorilla/handlers" 25 | "log" 26 | "net/http" 27 | "os" 28 | ) 29 | 30 | type TokenResponse struct { 31 | Token string `json:"token"` 32 | } 33 | 34 | type tokenHandler struct { 35 | keyPath string 36 | htpasswdPath string 37 | } 38 | 39 | func createAuthTokenFunc(keyPath string) func(w http.ResponseWriter, r *auth.AuthenticatedRequest) { 40 | return func(w http.ResponseWriter, r *auth.AuthenticatedRequest) { 41 | service := r.URL.Query().Get("service") 42 | scope, _ := token.ParseScope(r.URL.Query().Get("scope")) 43 | 44 | token, err := token.NewJwtToken(r.Username, service, scope, keyPath) 45 | log.Println(utils.PrettyPrint(token.Claim())) 46 | 47 | if err != nil { 48 | log.Println("error:", err) 49 | http.Error(w, err.Error(), http.StatusInternalServerError) 50 | return 51 | } 52 | 53 | signed, err := token.SignedString() 54 | if err != nil { 55 | log.Println("error:", err) 56 | http.Error(w, err.Error(), http.StatusInternalServerError) 57 | return 58 | } 59 | 60 | js, err := json.Marshal(TokenResponse{Token: signed}) 61 | if err != nil { 62 | log.Println("error:", err) 63 | http.Error(w, err.Error(), http.StatusInternalServerError) 64 | return 65 | } 66 | 67 | w.Header().Set("Content-Type", "application/json") 68 | w.Write(js) 69 | } 70 | } 71 | 72 | func NewGaritaTokenHandler(htpasswdPath string, keyPath string) http.Handler { 73 | validator := auth.NewHtpasswdValidator(htpasswdPath) 74 | tokenHandler := auth.BasicAuth(createAuthTokenFunc(keyPath), "realm", validator) 75 | logHandler := handlers.LoggingHandler(os.Stdout, auth.GetOnly(tokenHandler)) 76 | return logHandler 77 | } 78 | -------------------------------------------------------------------------------- /api/handler_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/stretchr/testify/assert" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | const htpasswdPath = "../vagrant/conf/htpasswd" 14 | const keyPath = "../vagrant/conf/ca_bundle/server.key" 15 | 16 | type tokenResp struct { 17 | Token string `json:"token"` 18 | } 19 | 20 | func TestUnauthorized(t *testing.T) { 21 | assert := assert.New(t) 22 | 23 | handler := NewGaritaTokenHandler(htpasswdPath, keyPath) 24 | recorder := httptest.NewRecorder() 25 | url := fmt.Sprintf("http://example.com/v2/token?account=duncan&service=registry.test.lan") 26 | req, err := http.NewRequest("GET", url, nil) 27 | assert.Nil(err) 28 | 29 | handler.ServeHTTP(recorder, req) 30 | 31 | assert.Equal(401, recorder.Code) 32 | } 33 | 34 | func TestTokenOutput(t *testing.T) { 35 | assert := assert.New(t) 36 | 37 | handler := NewGaritaTokenHandler(htpasswdPath, keyPath) 38 | recorder := httptest.NewRecorder() 39 | url := fmt.Sprintf("http://example.com/v2/token?account=duncan&service=registry.test.lan") 40 | req, err := http.NewRequest("GET", url, nil) 41 | req.SetBasicAuth("duncan", "garita") 42 | assert.Nil(err) 43 | 44 | handler.ServeHTTP(recorder, req) 45 | 46 | assert.Equal(200, recorder.Code) 47 | 48 | responseJson := new(tokenResp) 49 | err = json.Unmarshal(recorder.Body.Bytes(), responseJson) 50 | assert.Nil(err) 51 | 52 | // JWT tokens are XXX.YYY.ZZZ 53 | tokenParts := strings.Split(responseJson.Token, ".") 54 | assert.Equal(3, len(tokenParts)) 55 | } 56 | -------------------------------------------------------------------------------- /auth/basic.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // package main 16 | // 17 | package auth 18 | 19 | import ( 20 | "encoding/base64" 21 | "fmt" 22 | "net/http" 23 | "strings" 24 | ) 25 | 26 | func BasicAuth(pass AuthenticatedHandlerFunc, realm string, validator Validator) http.HandlerFunc { 27 | 28 | sendUnauthorized := func(w http.ResponseWriter, r *http.Request) { 29 | w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm)) 30 | http.Error(w, "authorization failed", http.StatusUnauthorized) 31 | } 32 | 33 | return func(w http.ResponseWriter, r *http.Request) { 34 | 35 | if len(r.Header["Authorization"]) < 1 { 36 | sendUnauthorized(w, r) 37 | return 38 | } 39 | 40 | authHeader := r.Header["Authorization"][0] 41 | auth := strings.SplitN(authHeader, " ", 2) 42 | 43 | if len(auth) != 2 || auth[0] != "Basic" { 44 | http.Error(w, "bad syntax", http.StatusBadRequest) 45 | return 46 | } 47 | 48 | payload, _ := base64.StdEncoding.DecodeString(auth[1]) 49 | pair := strings.SplitN(string(payload), ":", 2) 50 | 51 | if len(pair) != 2 || !validator(pair[0], pair[1]) { 52 | sendUnauthorized(w, r) 53 | return 54 | } 55 | 56 | //w.Header().Set("X-Authorized-Username", pair[0]) 57 | ar := &AuthenticatedRequest{Request: *r, Username: pair[0]} 58 | pass(w, ar) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /auth/htpasswd.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Based on https://github.com/abbot/go-http-auth 5 | // Copyright 2012-2013 Lev Shamardin 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | // package main 19 | // 20 | package auth 21 | 22 | import ( 23 | "crypto/sha1" 24 | "crypto/subtle" 25 | "encoding/base64" 26 | "encoding/csv" 27 | "os" 28 | "strings" 29 | ) 30 | 31 | /* 32 | SecretProvider is used by authenticators. Takes user name and realm 33 | as an argument, returns secret required for authentication (HA1 for 34 | digest authentication, properly encrypted password for basic). 35 | 36 | Returning an empty string means failing the authentication. 37 | */ 38 | type SecretProvider func(user, realm string) string 39 | 40 | /* 41 | Common functions for file auto-reloading 42 | */ 43 | type File struct { 44 | Path string 45 | Info os.FileInfo 46 | /* must be set in inherited types during initialization */ 47 | Reload func() 48 | } 49 | 50 | func (f *File) ReloadIfNeeded() { 51 | info, err := os.Stat(f.Path) 52 | if err != nil { 53 | panic(err) 54 | } 55 | if f.Info == nil || f.Info.ModTime() != info.ModTime() { 56 | f.Info = info 57 | f.Reload() 58 | } 59 | } 60 | 61 | /* 62 | Structure used for htdigest file authentication. Users map realms to 63 | maps of users to their HA1 digests. 64 | */ 65 | type HtdigestFile struct { 66 | File 67 | Users map[string]map[string]string 68 | } 69 | 70 | func reload_htdigest(hf *HtdigestFile) { 71 | r, err := os.Open(hf.Path) 72 | if err != nil { 73 | panic(err) 74 | } 75 | csv_reader := csv.NewReader(r) 76 | csv_reader.Comma = ':' 77 | csv_reader.Comment = '#' 78 | csv_reader.TrimLeadingSpace = true 79 | 80 | records, err := csv_reader.ReadAll() 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | hf.Users = make(map[string]map[string]string) 86 | for _, record := range records { 87 | _, exists := hf.Users[record[1]] 88 | if !exists { 89 | hf.Users[record[1]] = make(map[string]string) 90 | } 91 | hf.Users[record[1]][record[0]] = record[2] 92 | } 93 | } 94 | 95 | /* 96 | SecretProvider implementation based on htdigest-formated files. Will 97 | reload htdigest file on changes. Will panic on syntax errors in 98 | htdigest files. 99 | */ 100 | func HtdigestFileProvider(filename string) SecretProvider { 101 | hf := &HtdigestFile{File: File{Path: filename}} 102 | hf.Reload = func() { reload_htdigest(hf) } 103 | return func(user, realm string) string { 104 | hf.ReloadIfNeeded() 105 | _, exists := hf.Users[realm] 106 | if !exists { 107 | return "" 108 | } 109 | digest, exists := hf.Users[realm][user] 110 | if !exists { 111 | return "" 112 | } 113 | return digest 114 | } 115 | } 116 | 117 | /* 118 | Structure used for htdigest file authentication. Users map users to 119 | their salted encrypted password 120 | */ 121 | type HtpasswdFile struct { 122 | File 123 | Users map[string]string 124 | } 125 | 126 | func reload_htpasswd(h *HtpasswdFile) { 127 | r, err := os.Open(h.Path) 128 | if err != nil { 129 | panic(err) 130 | } 131 | csv_reader := csv.NewReader(r) 132 | csv_reader.Comma = ':' 133 | csv_reader.Comment = '#' 134 | csv_reader.TrimLeadingSpace = true 135 | 136 | records, err := csv_reader.ReadAll() 137 | if err != nil { 138 | panic(err) 139 | } 140 | 141 | h.Users = make(map[string]string) 142 | for _, record := range records { 143 | h.Users[record[0]] = record[1] 144 | } 145 | } 146 | 147 | /* 148 | SecretProvider implementation based on htpasswd-formated files. Will 149 | reload htpasswd file on changes. Will panic on syntax errors in 150 | htpasswd files. Realm argument of the SecretProvider is ignored. 151 | */ 152 | func HtpasswdFileProvider(filename string) SecretProvider { 153 | h := &HtpasswdFile{File: File{Path: filename}} 154 | h.Reload = func() { reload_htpasswd(h) } 155 | return func(user, realm string) string { 156 | h.ReloadIfNeeded() 157 | password, exists := h.Users[user] 158 | if !exists { 159 | return "" 160 | } 161 | return password 162 | } 163 | } 164 | 165 | func NewHtpasswdValidator(filename string) Validator { 166 | provider := HtpasswdFileProvider(filename) 167 | 168 | return func(username string, passwd string) bool { 169 | // realm is ignored 170 | hashedPw := provider(username, "") 171 | 172 | if strings.HasPrefix(hashedPw, "{SHA}") { 173 | d := sha1.New() 174 | d.Write([]byte(passwd)) 175 | 176 | if subtle.ConstantTimeCompare([]byte(hashedPw[5:]), []byte(base64.StdEncoding.EncodeToString(d.Sum(nil)))) != 1 { 177 | return false 178 | } 179 | } else { 180 | e := NewMD5Entry(hashedPw) 181 | if e == nil { 182 | return false 183 | } 184 | if subtle.ConstantTimeCompare([]byte(hashedPw), MD5Crypt([]byte(passwd), e.Salt, e.Magic)) != 1 { 185 | return false 186 | } 187 | } 188 | return true 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /auth/md5crypt.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Based on https://github.com/abbot/go-http-auth 5 | // Copyright 2012-2013 Lev Shamardin 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | // package main 19 | // 20 | package auth 21 | 22 | import "crypto/md5" 23 | import "strings" 24 | 25 | const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 26 | 27 | var md5_crypt_swaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11} 28 | 29 | type MD5Entry struct { 30 | Magic, Salt, Hash []byte 31 | } 32 | 33 | func NewMD5Entry(e string) *MD5Entry { 34 | parts := strings.SplitN(e, "$", 4) 35 | if len(parts) != 4 { 36 | return nil 37 | } 38 | return &MD5Entry{ 39 | Magic: []byte("$" + parts[1] + "$"), 40 | Salt: []byte(parts[2]), 41 | Hash: []byte(parts[3]), 42 | } 43 | } 44 | 45 | /* 46 | MD5 password crypt implementation 47 | */ 48 | func MD5Crypt(password, salt, magic []byte) []byte { 49 | d := md5.New() 50 | 51 | d.Write(password) 52 | d.Write(magic) 53 | d.Write(salt) 54 | 55 | d2 := md5.New() 56 | d2.Write(password) 57 | d2.Write(salt) 58 | d2.Write(password) 59 | 60 | for i, mixin := 0, d2.Sum(nil); i < len(password); i++ { 61 | d.Write([]byte{mixin[i%16]}) 62 | } 63 | 64 | for i := len(password); i != 0; i >>= 1 { 65 | if i&1 == 0 { 66 | d.Write([]byte{password[0]}) 67 | } else { 68 | d.Write([]byte{0}) 69 | } 70 | } 71 | 72 | final := d.Sum(nil) 73 | 74 | for i := 0; i < 1000; i++ { 75 | d2 := md5.New() 76 | if i&1 == 0 { 77 | d2.Write(final) 78 | } else { 79 | d2.Write(password) 80 | } 81 | 82 | if i%3 != 0 { 83 | d2.Write(salt) 84 | } 85 | 86 | if i%7 != 0 { 87 | d2.Write(password) 88 | } 89 | 90 | if i&1 == 0 { 91 | d2.Write(password) 92 | } else { 93 | d2.Write(final) 94 | } 95 | final = d2.Sum(nil) 96 | } 97 | 98 | result := make([]byte, 0, 22) 99 | v := uint(0) 100 | bits := uint(0) 101 | for _, i := range md5_crypt_swaps { 102 | v |= (uint(final[i]) << bits) 103 | for bits = bits + 8; bits > 6; bits -= 6 { 104 | result = append(result, itoa64[v&0x3f]) 105 | v >>= 6 106 | } 107 | } 108 | result = append(result, itoa64[v&0x3f]) 109 | 110 | return append(append(append(magic, salt...), '$'), result...) 111 | } 112 | -------------------------------------------------------------------------------- /auth/md5crypt_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Based on https://github.com/abbot/go-http-auth 5 | // Copyright 2012-2013 Lev Shamardin 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | // package main 19 | // 20 | package auth 21 | 22 | import "testing" 23 | 24 | func Test_MD5Crypt(t *testing.T) { 25 | test_cases := [][]string{ 26 | {"apache", "$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1"}, 27 | {"pass", "$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90"}, 28 | {"topsecret", "$apr1$JI4wh3am$AmhephVqLTUyAVpFQeHZC0"}, 29 | } 30 | for _, tc := range test_cases { 31 | e := NewMD5Entry(tc[1]) 32 | result := MD5Crypt([]byte(tc[0]), e.Salt, e.Magic) 33 | if string(result) != tc[1] { 34 | t.Fatalf("MD5Crypt returned '%s' instead of '%s'", string(result), tc[1]) 35 | } 36 | t.Logf("MD5Crypt: '%s' (%s%s$) -> %s", tc[0], e.Magic, e.Salt, result) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /auth/request.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Based on https://github.com/abbot/go-http-auth 5 | // Copyright 2012-2013 Lev Shamardin 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | // package main 19 | // 20 | package auth 21 | 22 | import "net/http" 23 | 24 | /* 25 | Request handlers must take AuthenticatedRequest instead of http.Request 26 | */ 27 | type AuthenticatedRequest struct { 28 | http.Request 29 | /* 30 | Authenticated user name. Current API implies that Username is 31 | never empty, which means that authentication is always done 32 | before calling the request handler. 33 | */ 34 | Username string 35 | } 36 | 37 | /* 38 | AuthenticatedHandlerFunc is like http.HandlerFunc, but takes 39 | AuthenticatedRequest instead of http.Request 40 | */ 41 | type AuthenticatedHandlerFunc func(http.ResponseWriter, *AuthenticatedRequest) 42 | -------------------------------------------------------------------------------- /auth/validate.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // package main 16 | // 17 | package auth 18 | 19 | type Validator func(username string, passwd string) bool 20 | -------------------------------------------------------------------------------- /auth/wrappers.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Based on https://github.com/abbot/go-http-auth 5 | // Copyright 2012-2013 Lev Shamardin 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | // package main 19 | // 20 | package auth 21 | 22 | import ( 23 | "net/http" 24 | ) 25 | 26 | func GetOnly(h http.HandlerFunc) http.HandlerFunc { 27 | 28 | return func(w http.ResponseWriter, r *http.Request) { 29 | if r.Method == "GET" { 30 | h(w, r) 31 | return 32 | } 33 | http.Error(w, "get only", http.StatusMethodNotAllowed) 34 | } 35 | } 36 | 37 | func PostOnly(h http.HandlerFunc) http.HandlerFunc { 38 | 39 | return func(w http.ResponseWriter, r *http.Request) { 40 | if r.Method == "POST" { 41 | h(w, r) 42 | return 43 | } 44 | http.Error(w, "post only", http.StatusMethodNotAllowed) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /commands/garita.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | 13 | "github.com/dmacvicar/garita/api" 14 | ) 15 | 16 | var GaritaCmd = &cobra.Command{ 17 | Use: "garita", 18 | Short: "light v2 auth server for docker", 19 | Long: "starts the garita server", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | initializeConfig() 22 | server() 23 | }, 24 | } 25 | 26 | var garitaCmdV *cobra.Command 27 | 28 | // cmd line flags 29 | var insecureHttpF bool 30 | var listenPortF int 31 | var htpasswdPathF string 32 | var keyPathF string 33 | var tlsCertPathF string 34 | var tlsKeyPathF string 35 | var configPathF string 36 | 37 | //Execute adds all child commands to the root command GaritaCmd and sets flags appropriately. 38 | func Execute() { 39 | // add the subcommands here in the future if any 40 | GaritaCmd.Execute() 41 | } 42 | 43 | func init() { 44 | GaritaCmd.Flags().StringVarP(&keyPathF, "key", "k", "./server.key", "Auth token secret key") 45 | GaritaCmd.Flags().BoolVarP(&insecureHttpF, "http", "x", false, "use plain HTTP") 46 | GaritaCmd.Flags().IntVarP(&listenPortF, "port", "p", 443, "Port to listen to") 47 | GaritaCmd.Flags().StringVarP(&htpasswdPathF, "htpasswd", "w", "./htpasswd", "htpasswd file") 48 | GaritaCmd.Flags().StringVarP(&tlsCertPathF, "tlscert", "s", "./server.crt", "TLS certificate") 49 | GaritaCmd.Flags().StringVarP(&tlsKeyPathF, "tlskey", "y", "./server.key", "TLS secret key") 50 | 51 | GaritaCmd.Flags().StringVarP(&configPathF, "config", "c", "", "Configuration file. Command line options override settings in the configuration file") 52 | 53 | garitaCmdV = GaritaCmd 54 | } 55 | 56 | func initializeConfig() { 57 | 58 | if garitaCmdV.Flags().Lookup("config").Changed { 59 | viper.SetConfigFile(configPathF) 60 | err := viper.ReadInConfig() 61 | if err != nil { 62 | log.Println("Unable to locate configuration file: " + configPathF) 63 | } else { 64 | log.Println(fmt.Sprintf("Using configuration %s", viper.ConfigFileUsed())) 65 | } 66 | } 67 | 68 | viper.SetDefault("key", "server.key") 69 | viper.SetDefault("port", 443) 70 | viper.SetDefault("http", false) 71 | viper.SetDefault("htpasswd", "htpasswd") 72 | viper.SetDefault("tlscert", "server.crt") 73 | viper.SetDefault("tlskey", "server.key") 74 | 75 | if garitaCmdV.Flags().Lookup("key").Changed { 76 | viper.Set("key", &keyPathF) 77 | } 78 | 79 | if garitaCmdV.Flags().Lookup("http").Changed { 80 | viper.Set("http", &insecureHttpF) 81 | } 82 | 83 | if garitaCmdV.Flags().Lookup("port").Changed { 84 | viper.Set("port", &listenPortF) 85 | } 86 | 87 | if garitaCmdV.Flags().Lookup("htpasswd").Changed { 88 | viper.Set("htpasswd", &htpasswdPathF) 89 | } 90 | 91 | if garitaCmdV.Flags().Lookup("tlskey").Changed { 92 | viper.Set("tlskey", &tlsKeyPathF) 93 | } 94 | 95 | if garitaCmdV.Flags().Lookup("tlscert").Changed { 96 | viper.Set("tlscert", &tlsCertPathF) 97 | } 98 | } 99 | 100 | func server() { 101 | 102 | insecureHttp := viper.GetBool("http") 103 | listenPort := viper.GetInt("port") 104 | htpasswdPath := viper.GetString("htpasswd") 105 | keyPath := viper.GetString("key") 106 | tlsCertPath := viper.GetString("tlscert") 107 | tlsKeyPath := viper.GetString("tlskey") 108 | 109 | if _, err := os.Stat(htpasswdPath); os.IsNotExist(err) { 110 | fmt.Printf("no such file or directory: %s", htpasswdPath) 111 | return 112 | } 113 | 114 | if _, err := os.Stat(keyPath); os.IsNotExist(err) { 115 | fmt.Printf("no such file or directory: %s", keyPath) 116 | return 117 | } 118 | 119 | // tls requires both cert and key 120 | if !insecureHttp { 121 | if _, err := os.Stat(tlsCertPath); os.IsNotExist(err) { 122 | fmt.Printf("no such file or directory: %s", tlsCertPath) 123 | return 124 | } 125 | 126 | if _, err := os.Stat(tlsKeyPath); os.IsNotExist(err) { 127 | fmt.Printf("no such file or directory: %s", tlsKeyPath) 128 | return 129 | } 130 | } 131 | 132 | tokenHandler := api.NewGaritaTokenHandler(htpasswdPath, keyPath) 133 | 134 | router := mux.NewRouter() 135 | router.Handle("/v2/token", tokenHandler) 136 | log.Printf("Listening...:%d", listenPort) 137 | 138 | if insecureHttp { 139 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", listenPort), router)) 140 | } else { 141 | log.Fatal(http.ListenAndServeTLS(fmt.Sprintf(":%d", listenPort), tlsCertPath, tlsKeyPath, router)) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /docker/images/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM opensuse:13.2 2 | MAINTAINER Duncan Mac-Vicar P. "dmacvicar@suse.com" 3 | RUN zypper --non-interactive ar -f http://download.opensuse.org/repositories/Virtualization/openSUSE_13.2/Virtualization.repo 4 | #RUN zypper --non-interactive ar -f http://download.opensuse.org/repositories/Virt#ualization:/containers/openSUSE_13.2/Virtualization:containers.repo 5 | RUN zypper --gpg-auto-import-keys --non-interactive in --no-recommends \ 6 | docker curl 7 | # directory for the garita auth server CA cert 8 | RUN mkdir -p /etc/docker/certs.d/garita.test.lan 9 | EXPOSE 2375 10 | CMD ["/usr/bin/docker", "daemon", "-H", "tcp://0.0.0.0:2375", "--insecure-registry", "registry.test.lan", "-s", "overlay"] 11 | -------------------------------------------------------------------------------- /docker/images/registry/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM opensuse:13.2 2 | MAINTAINER Duncan Mac-Vicar P. "dmacvicar@suse.com" 3 | RUN zypper --non-interactive ar -f http://download.opensuse.org/repositories/Virtualization:/containers/openSUSE_13.2/Virtualization:containers.repo 4 | RUN zypper --gpg-auto-import-keys --non-interactive in --no-recommends \ 5 | docker-distribution-registry curl 6 | EXPOSE 80 7 | 8 | CMD ["/usr/bin/registry", "/etc/registry-config.yml"] 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // package main 16 | // 17 | package main 18 | 19 | import ( 20 | "github.com/dmacvicar/garita/commands" 21 | ) 22 | 23 | func main() { 24 | commands.Execute() 25 | } 26 | -------------------------------------------------------------------------------- /token/jwt_token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "crypto/rsa" 5 | libjwt "github.com/dgrijalva/jwt-go" 6 | utils "github.com/dmacvicar/garita/utils" 7 | uuid "github.com/nu7hatch/gouuid" 8 | "io/ioutil" 9 | "os" 10 | "time" 11 | ) 12 | 13 | type JwtToken struct { 14 | Account string 15 | Service string 16 | Scope *Scope 17 | privKey *rsa.PrivateKey 18 | } 19 | 20 | type accessItem struct { 21 | Type string `json:"type"` 22 | Name string `json:"name"` 23 | Actions []string `json:"actions"` 24 | } 25 | 26 | func NewJwtToken(account string, service string, scope *Scope, keyPath string) (*JwtToken, error) { 27 | token := new(JwtToken) 28 | token.Account = account 29 | token.Service = service 30 | token.Scope = scope 31 | 32 | if err := token.parsePrivateKey(keyPath); err != nil { 33 | return nil, err 34 | } 35 | return token, nil 36 | } 37 | 38 | func (t *JwtToken) jwtKid() (string, error) { 39 | if kid, err := utils.KeyIDFromCryptoKey(t.privKey.Public()); err != nil { 40 | return "", err 41 | } else { 42 | return kid, nil 43 | } 44 | } 45 | 46 | func (t *JwtToken) parsePrivateKey(keyPath string) error { 47 | if pem, err := ioutil.ReadFile(keyPath); err != nil { 48 | return err 49 | } else { 50 | if privKey, err := libjwt.ParseRSAPrivateKeyFromPEM(pem); err != nil { 51 | return err 52 | } else { 53 | t.privKey = privKey 54 | return nil 55 | } 56 | 57 | } 58 | } 59 | 60 | func (t *JwtToken) notBefore() time.Time { 61 | return time.Now().Add(time.Second * -5) 62 | } 63 | 64 | func (t *JwtToken) issuedAt() time.Time { 65 | return t.notBefore() 66 | } 67 | 68 | func (t *JwtToken) expires() time.Time { 69 | return time.Now().Add(time.Minute * 5) 70 | } 71 | 72 | func (t *JwtToken) issuer() time.Time { 73 | return time.Now().Add(time.Minute * 5) 74 | } 75 | 76 | func (t *JwtToken) jwtId() (string, error) { 77 | if jti, err := uuid.NewV4(); err != nil { 78 | return "", err 79 | } else { 80 | return jti.String(), nil 81 | } 82 | } 83 | 84 | func (t *JwtToken) singleAction() accessItem { 85 | action := accessItem{} 86 | action.Type = t.Scope.Type 87 | action.Name = t.Scope.Name 88 | 89 | // only allow push pull if scope namespace 90 | // is the same as the authenticated account 91 | if t.Account == t.Scope.Namespace { 92 | action.Actions = t.Scope.Actions 93 | } else { 94 | action.Actions = []string{} 95 | } 96 | return action 97 | } 98 | 99 | func (t *JwtToken) authorizedAccess() []accessItem { 100 | return []accessItem{t.singleAction()} 101 | } 102 | 103 | func (t *JwtToken) Claim() libjwt.Claims { 104 | 105 | claims := make(libjwt.MapClaims) 106 | 107 | fqdn, err := os.Hostname() 108 | if err != nil { 109 | claims["iss"] = "garita" 110 | } else { 111 | claims["iss"] = fqdn 112 | } 113 | 114 | claims["sub"] = t.Account 115 | claims["aud"] = t.Service 116 | 117 | claims["exp"] = t.expires().Unix() 118 | claims["nbf"] = t.notBefore().Unix() 119 | claims["iat"] = t.issuedAt().Unix() 120 | 121 | if id, err := t.jwtId(); err != nil { 122 | claims["jti"] = id 123 | } 124 | 125 | if t.Scope != nil { 126 | claims["access"] = t.authorizedAccess() 127 | } 128 | 129 | return claims 130 | } 131 | 132 | func (t *JwtToken) SignedString() (string, error) { 133 | 134 | // now create the token 135 | jwt := libjwt.New(libjwt.SigningMethodRS256) 136 | jwt.Claims = t.Claim() 137 | if kid, err := t.jwtKid(); err != nil { 138 | return "", err 139 | } else { 140 | jwt.Header["kid"] = kid 141 | } 142 | 143 | signed, err := jwt.SignedString(t.privKey) 144 | if err != nil { 145 | return "", err 146 | } 147 | return signed, nil 148 | } 149 | -------------------------------------------------------------------------------- /token/jwt_token_test.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | libjwt "github.com/dgrijalva/jwt-go" 5 | utils "github.com/dmacvicar/garita/utils" 6 | "github.com/stretchr/testify/assert" 7 | "log" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestJwtTokenProperties(t *testing.T) { 13 | assert := assert.New(t) 14 | 15 | const keyPath = "../vagrant/conf/ca_bundle/server.key" 16 | 17 | scope := NewScope("repository", "duncan", []string{"push", "pull"}) 18 | 19 | token, err := NewJwtToken("duncan", "registry.test.lan", scope, keyPath) 20 | assert.Nil(err) 21 | 22 | kid, err := token.jwtKid() 23 | assert.Nil(err) 24 | 25 | assert.Equal("NSN7:VDFR:FTW6:WBBB:7WQK:ABNJ:7CI5:M6YU:7FSD:QS45:A2BR:PAMO", kid) 26 | 27 | claims := token.Claim().(libjwt.MapClaims) 28 | 29 | log.Printf(utils.PrettyPrint(claims)) 30 | assert.Equal("registry.test.lan", claims["aud"]) 31 | 32 | signed, err := token.SignedString() 33 | assert.Nil(err) 34 | 35 | tokenParts := strings.Split(signed, ".") 36 | assert.Equal(3, len(tokenParts)) 37 | } 38 | -------------------------------------------------------------------------------- /token/scope.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type Scope struct { 10 | Type string 11 | Name string 12 | Namespace string 13 | Actions []string 14 | } 15 | 16 | func NewScope(typ string, name string, actions []string) *Scope { 17 | 18 | var namespace string 19 | if strings.Contains(name, "/") { 20 | namespace = strings.Split(name, "/")[0] 21 | } 22 | return &Scope{ 23 | Type: typ, 24 | Name: name, 25 | Namespace: namespace, 26 | Actions: actions, 27 | } 28 | } 29 | 30 | func ParseScope(scopeString string) (*Scope, error) { 31 | 32 | parts := strings.Split(scopeString, ":") 33 | if len(parts) != 3 { 34 | return nil, errors.New(fmt.Sprintf("invalid scope string: '%v'", scopeString)) 35 | } 36 | 37 | return NewScope(parts[0], parts[1], strings.Split(parts[2], ",")), nil 38 | } 39 | -------------------------------------------------------------------------------- /token/scope_test.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParse(t *testing.T) { 9 | 10 | scope, err := ParseScope("repository:duncan/busybox:pull,push") 11 | 12 | if err != nil { 13 | t.Fail() 14 | } 15 | 16 | if scope.Name != "duncan/busybox" { 17 | t.Errorf(scope.Name) 18 | } 19 | 20 | if scope.Type != "repository" { 21 | t.Errorf(scope.Type) 22 | } 23 | 24 | if scope.Namespace != "duncan" { 25 | t.Errorf(scope.Name) 26 | } 27 | 28 | if !reflect.DeepEqual(scope.Actions, []string{"pull", "push"}) { 29 | t.Errorf("%v", scope.Actions) 30 | 31 | } 32 | } 33 | 34 | func TestParseBroken(t *testing.T) { 35 | 36 | _, err := ParseScope("duncan/busybox:pull,push") 37 | 38 | if err == nil { 39 | t.Fail() 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 SUSE LLC. All rights reserved. 3 | // 4 | // Contains code based on libtrust 5 | // Copyright 2014 Docker, Inc. 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | // package main 19 | // 20 | package utils 21 | 22 | import ( 23 | "bytes" 24 | "crypto" 25 | "crypto/x509" 26 | "encoding/base32" 27 | "encoding/json" 28 | "strings" 29 | ) 30 | 31 | func keyIDEncode(b []byte) string { 32 | s := strings.TrimRight(base32.StdEncoding.EncodeToString(b), "=") 33 | var buf bytes.Buffer 34 | var i int 35 | for i = 0; i < len(s)/4-1; i++ { 36 | start := i * 4 37 | end := start + 4 38 | buf.WriteString(s[start:end] + ":") 39 | } 40 | buf.WriteString(s[i*4:]) 41 | return buf.String() 42 | } 43 | 44 | func KeyIDFromCryptoKey(pubKey crypto.PublicKey) (string, error) { 45 | // Generate and return a 'libtrust' fingerprint of the public key. 46 | // For an RSA key this should be: 47 | // SHA256(DER encoded ASN1) 48 | // Then truncated to 240 bits and encoded into 12 base32 groups like so: 49 | // ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP 50 | derBytes, err := x509.MarshalPKIXPublicKey(pubKey) 51 | if err != nil { 52 | return "", err 53 | } 54 | hasher := crypto.SHA256.New() 55 | hasher.Write(derBytes) 56 | return keyIDEncode(hasher.Sum(nil)[:30]), nil 57 | } 58 | 59 | func PrettyPrint(x interface{}) string { 60 | b, err := json.MarshalIndent(x, "", " ") 61 | if err != nil { 62 | return err.Error() 63 | } 64 | return string(b) 65 | } 66 | -------------------------------------------------------------------------------- /vagrant/conf/ca_bundle/README.md: -------------------------------------------------------------------------------- 1 | Password: `garita` 2 | -------------------------------------------------------------------------------- /vagrant/conf/ca_bundle/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF1TCCA72gAwIBAgIJAPhfAazNw1iEMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYD 3 | VQQGEwJERTEPMA0GA1UECAwGQmF5ZXJuMRIwEAYDVQQHDAlOdXJlbWJlcmcxDTAL 4 | BgNVBAoMBFNVU0UxGDAWBgNVBAMMD2dhcml0YS50ZXN0LmxhbjEjMCEGCSqGSIb3 5 | DQEJARYUcm9vdEBnYXJpdGEudGVzdC5sYW4wHhcNMTUwOTA4MDg1MDQyWhcNMjAw 6 | OTA3MDg1MDQyWjCBgDELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJheWVybjESMBAG 7 | A1UEBwwJTnVyZW1iZXJnMQ0wCwYDVQQKDARTVVNFMRgwFgYDVQQDDA9nYXJpdGEu 8 | dGVzdC5sYW4xIzAhBgkqhkiG9w0BCQEWFHJvb3RAZ2FyaXRhLnRlc3QubGFuMIIC 9 | IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyRnK7KKxeB2Q83nbrLOe3FLk 10 | Z4if9wF4n2Tys9xxaNINA56H8UaY7V8p3C29FirIKhnHqQtgqKnB+jDpZ2BQ+PvN 11 | oDcJO9HnoTrqqqbud+Gz9BA7oeopRZY/mNx7pvxSGq/QOEkFFP2WS7KboKmjQLh3 12 | 4Bmfoctco2RwIv3RS1gl4X37t6Dpb3iOfXbltrpdtga/vm6att6SwjiU4YRrn8aW 13 | Zbm7Njy/nJCrnR20LA5iYCc7MvMJyW6r90kjZ7JjGIJOOqt2SXoOkQN43u8u13kD 14 | WAKEBsQHOlP5PW2XocoOj+xc3jlvAdXrSEABimI0LbBlsakqSAjsib1kEHt36ZDW 15 | 6t2kyj0V8Evcd+GvjF8B8mcXrUIDQtDb+W3YpZYZjt5gl4e4rQMcHe/eq/7w7jVu 16 | 1YuOsJn7G4NXnA906IbPzeN25Sk4JCCqn6CJu/QHcyTWqBv/CRbXd51WmzgES5/C 17 | 4Mw7QxDxDNqmNbrM21PygZhRhIYulOnHGtHHPhQmrMC+YcnnlG8xgkshSvf7KTlT 18 | b6ekXq4zqm3WyL5l+NNbVp+kEOxp5DXXzk1euAMYzcrOa/uaAdWE333Mtvz77+YA 19 | oN5CEbluiP/v2mRScX0GTA9LsusHdKBfDT3C8xJRu6phwpqwm+KRDdUVh2JsiLKq 20 | 2EDdRbRLa8FVi/IIDc0CAwEAAaNQME4wHQYDVR0OBBYEFOHuYdaoxJs6xKJ4SPuL 21 | ibfKZd0KMB8GA1UdIwQYMBaAFOHuYdaoxJs6xKJ4SPuLibfKZd0KMAwGA1UdEwQF 22 | MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAIViSY3urs2rmcJVOv8xcKfsPxYGFUU8 23 | yNBwZZsZlrhU5Am+c0m8FNYVSW4q0fPnytjeeWnoSvYM0tUs/oL4OzlbnkGrphJJ 24 | 2+xOmPqtZRS/d0F64fdnXbO6WZAzcXs+uZ+dAFKOGCywpdShC02HjeiF54sF9Pb+ 25 | zYX+1i0JYOWeTsHcacInM4/osMCCXGgOiVpMhdSK5ro3BygHO+8neUERkruURtjA 26 | /GzfrQEkKvUsrOO6rgckhnI6dvRKJy9VCwAaU09S9Z7UMDFPNd0Ir6fj7lyBHadd 27 | Jyc5swgoKqt10yazuLaOGGg1yMDUWJfJvlAUYE/g7tll85EmbNx8WrKapnRLJMeC 28 | hChJ7+mz8iPGKXLL7faMaDA0J0MiUbcu1TksBA67tXHwbct++QodaOkDmVhzWhqA 29 | SteOT/jQtAA108Ap90R55UHIceDZH7QoHt4Ldyi07cn3itP+Ov5jdQevIDrLVyso 30 | oOcv77R6Bb28raplTeda2aNFjOSZBGhAhCCQE6zXVo1cUNcCnPb29OtNn+aEc6bx 31 | tIYqQ9tszjzt1bf0cyWr/BA+vC6XHSVMu7K7ynIKrIqGQSLjZ7/I+QhXkZxKFB4E 32 | LiUj4zLn4749b4NdcmD9n2bHk4qJZUAnMiRKOVzPfjjes2XVlZDcMpg3yB5jsC+j 33 | CbLA2WHm4Fjl 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /vagrant/conf/ca_bundle/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKgIBAAKCAgEAyRnK7KKxeB2Q83nbrLOe3FLkZ4if9wF4n2Tys9xxaNINA56H 3 | 8UaY7V8p3C29FirIKhnHqQtgqKnB+jDpZ2BQ+PvNoDcJO9HnoTrqqqbud+Gz9BA7 4 | oeopRZY/mNx7pvxSGq/QOEkFFP2WS7KboKmjQLh34Bmfoctco2RwIv3RS1gl4X37 5 | t6Dpb3iOfXbltrpdtga/vm6att6SwjiU4YRrn8aWZbm7Njy/nJCrnR20LA5iYCc7 6 | MvMJyW6r90kjZ7JjGIJOOqt2SXoOkQN43u8u13kDWAKEBsQHOlP5PW2XocoOj+xc 7 | 3jlvAdXrSEABimI0LbBlsakqSAjsib1kEHt36ZDW6t2kyj0V8Evcd+GvjF8B8mcX 8 | rUIDQtDb+W3YpZYZjt5gl4e4rQMcHe/eq/7w7jVu1YuOsJn7G4NXnA906IbPzeN2 9 | 5Sk4JCCqn6CJu/QHcyTWqBv/CRbXd51WmzgES5/C4Mw7QxDxDNqmNbrM21PygZhR 10 | hIYulOnHGtHHPhQmrMC+YcnnlG8xgkshSvf7KTlTb6ekXq4zqm3WyL5l+NNbVp+k 11 | EOxp5DXXzk1euAMYzcrOa/uaAdWE333Mtvz77+YAoN5CEbluiP/v2mRScX0GTA9L 12 | susHdKBfDT3C8xJRu6phwpqwm+KRDdUVh2JsiLKq2EDdRbRLa8FVi/IIDc0CAwEA 13 | AQKCAgEAor6R33FGqAtdW/z5D0mJvYoDt9n0guQY5v2+AFrdGNQsngo0v8i/SBJk 14 | gQAu7vqOZKvaTe6cOcKv7baZnQRwYx78aLVBbrzPdEaG1LYldLUeedyNNdqXre4K 15 | 570/AINgOqKfon1NdJBIilgv5BSEvoLK2HxEGJ7ICJ7mtRqtvwjGFzdqd0/suj5Z 16 | KiYHfxpRblcF46oE4Qs8v5skuWD97B69ZfOqExmUg7L8fzkjryew61m0aeYPvIko 17 | +AWjdm8CDTqe7pIGNy5lDWw+7EOqp8wSLa5ThFot80E0CkdfmBo6MLU67siSEm05 18 | bI4H8SLKUVNY6S8avZMjQdEYYtJY1ifCJDftbdIePh50O0YFxtwqi3ztPQYVyA+C 19 | 41LBT/h2L5il3jmAgIRXyx765lZuTI7N1RUYC2Y+IBvnsEaJfjoIXgZa8piDgnr+ 20 | brKp7E679NkxI47CbEgFvj4pN5qU9mtiX67wyLZDpu64GFKVoB+Wyll+1ucEbxDb 21 | ejXjkDukSFczJWiUUYPJ/VN4qLxtyZGRSovoRKUZodlVHmM1UbN6o9t8fa8N5PoA 22 | a+oQOAOM1ZrQcSJpYR1cbR/NQlp7hn5WwpoaTpaYGg1tf8Vy/4TT2sVQ1E6zHLhk 23 | Z5QuCa+Pxorxhdr+QqAqlh/Y0zrhdx6OKWA32xxMkWuW2+UHis0CggEBAOzD+RkT 24 | wngukH4xrK/x8PEj47c3NCpSdpX+Yux8dicQYOo9uEvs3zOC/VetvQGkTRud39E5 25 | d8ZRK3p/rUNak9Sp+IUThlfLgzJE+1b+MXJ7VhxIxyKHeCggtBcNJuJbd5OMi4b/ 26 | tB7QGBL4I1eAVZC1e1Str2bMfvG/EUHO3jkis/b1ObvjrrCK46FiAK2gefBLuKIG 27 | 6RAw+0g+byVjfdcFOVmwYZAHlnz0IMQUuwhIkNLRRM3kBrCEWW0s/m+aVqYD5uby 28 | he7Sz1b/+iv/biIus2RFLI5k1+TxjdMEF+yNnl9yQlSSml3IwxhopdBbVlNguIu9 29 | XZjfN+trnYX0EDcCggEBANlwGOh+honANo4LDSz58WUktY197c2vz/lhYBboB3p5 30 | g4d1SSWM9WuphZPVGg68TXnH+4exslX02SGSf4YF/KgxKCtXARpnw2MxJs3TbU9t 31 | 3t47k/LXBWgDAwHhV1EV8/m2SDZeXcKiyrUoyeZ3zRocpEJUu8cUZxAXKNGwPerL 32 | 9iVH1Vk5JEGfyNF7Tus/t5/SZHt8pOExkndQNa9Ayk5Hlr1SgJEq1C24eTJd9NVL 33 | mK9OcRZnjWX+F+zsQ4vt0CGsbr2hdC8W9eBylczlFlgqk34ucwSrUpb61PqrLETb 34 | s9yjOUxAZN8yhOhh5WjI7MAFwJagr0LL7KsQlQjsaBsCggEAHbFaHQ4AVogocNsT 35 | +CDUgblphoGy1hfvbVIw382gF7gTH21MHqF/QHuOAB/20yziyrLa7edSIRnu5Pb8 36 | KLVEUuhaFX5kW8BXHMfP6ZIJa30SSIvMBYWNySKI01c/6CVmcqeum+iXk0GvszlR 37 | XVjn4jUQWYdKtw6wYOsAZAtojSvP/Am3Ctw7/UH92DBtO1kj0cH9TrylH/W0ndPO 38 | ppa6omuyTJA1PBXnhpYrQcwDxL8tAnNiOJv/RsXiXTHGWwK1GS7mF1KU/2WKwTNr 39 | EXeAPWMz9kBCnT0CYmD002DxCyablQbEUMzt9RMHfJkrKl06D0zS+BtwkIp1/d1K 40 | LRF1swKCAQEA1eyhtOv2tcQ0aF+bIOrvgpExRfnlYv5cYYBaJ281HFw8+aJ8ysL6 41 | Gmh4kU+SCXldyDjm0UQUrtjsX0lgyZ1OTLPAT5OT9ESuSQLsqIgg/YqdQIvhLYQ7 42 | P2/nH4xyecZdeQrL1SHWoU3EnaN5sJOhaNVRDimLbXw3Qe9nK5Z39G+ixl78GoJL 43 | l3z6+I6WwIC5yjYWwh+JoD1DWrvBdaEK6SUpuy5Wa3x1sF3Ub6dc4BU2LRPB00ci 44 | KHxrUZ/JfPwGT3r0oQEd685tLdvOYf3pk+ifn/9XDfr4kK5LqCuMrFpzhNnZ35lj 45 | JOzxMihOql8z/mAqcIe6pr83I752wobEjQKCAQEAic1MX+MxE+o8NBAtEdKZn7/7 46 | lQ808aM0voccYw47j9MpCUXapKT5PG4SqvBRFrQanWcL+eeSCGZpSUb7mjJ2pNSF 47 | gwwmmBHE18+h2L/RJtDoJ8+YT9y2aQLAzvVzIpV37gtNB9Y9z4RBieLTTbl0EkVI 48 | o0ePZmPa5oNAhCRGcQUCfOxQNO5O0FPAMgv0PUXZcHbJp5NjR/WK6XjPbfK/c73I 49 | LHEUdX9SkNVlH+u15GuuVokhsmtL1SvJtU6IC4IfkJUUcpcXcOnDF9e3XclHvcTG 50 | 2xiq/gVaD1WlvxocQACMXd1QRAqlob7fVcALNw+LFPUcTTNL67Vi3k9Wc4veGg== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /vagrant/conf/ca_bundle/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | UBpTf5Rbwqkn6Q== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /vagrant/conf/ca_bundle/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIE3TCCAsUCAQAwgYAxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xEjAQ 3 | BgNVBAcMCU51cmVtYmVyZzENMAsGA1UECgwEU1VTRTEYMBYGA1UEAwwPZ2FyaXRh 4 | LnRlc3QubGFuMSMwIQYJKoZIhvcNAQkBFhRyb290QGdhcml0YS50ZXN0LmxhbjCC 5 | AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO6h5ciS8YrCd9DBf+k73DG8 6 | W0qi4SzBV4SWjw4IZqgkgWmWxVjpJTWGiCB3L0GKinnlmTI1zUVBUyjb3tP96SCm 7 | rSKYZfINe3EQAqKZV+koDcEX4Y0c29CCwezZqTjdBP/JIO8kEibEeAzVvlldOuZl 8 | PiIvbUsek9AG+wl3mMrRJ6yN9F+hcrQNaUiZ3YECZYRt1zo9V22MpHv16400Mudx 9 | aB3WPPY3spTmNPfTC2nKld9HZM+yPTV6WixNIYy9n2FQSnmATzWKVVQMnbizJwVM 10 | V/6/qZZmNdtS+EoY5iULtYvSjcJRYjCYwzOEVRwQAIO84SjNut9hxLFu5eGckvEq 11 | Z5Gal3dCNd5WlN+EEFV+bOq+41h2heRYsiLb7/C9YCu4erHc3YkmY31NW0ARE3Q6 12 | 7i4Q79ME1DgSSRVFhHilGVoEjfroN6L5Rmb5iuXX6+d0QUeJ8AG/2BTGBCWWVBZF 13 | fnQHiI+Q8OYjSl6OpRMMi7aT82U9TkCicLbApehLp5ruZkYgnMEv+qfolQFqGcIL 14 | YH23Q0hrWfgHSSALS2PGbyOD170B7/wkT6hfZEp6Qpeg8sMjiwqHUsexeeHjX81D 15 | Kgb7PT3Wb9t/QrYK0cIRee8EpbrCvWsZLnpL4e+Q+n4xFW4AvRKH7xro8POSaJgI 16 | 5wEmnXh6zVfg0UNVeVgXAgMBAAGgFzAVBgkqhkiG9w0BCQcxCAwGZ2FyaXRhMA0G 17 | CSqGSIb3DQEBCwUAA4ICAQDNLbT2nCT04cHbGTqpTr+CHHflgdWNpoIhTKriGVWi 18 | erNT2QtQNsDh/IRdv1fX+I+lOIue407SOz4SXXK16XtrOk9FUqckE+AStxpi0GVZ 19 | WSy8z30lA49qutrcBfPnx3jf12XmPChIo0PQUWW/Zw4leTD2dNrjiM5P7YFgSxxH 20 | alyEiskGnPwX6rNAlKYG9HUvwIfHIsFfYTCNk+4kJvBmzY7e4dZCl4pCRaAc7tCD 21 | C6a2Q9u3R9Ukj593i4V+/DkqTBJmac75WxiVSRWzQMwyIAVUbl6MGJZEj8fjy21r 22 | dw7u1/1/jkFc0W+qVbmSqdrnGGaWaLUPkKjSAwCHIrKcofGG+L/2A/UuDJL9mZRV 23 | 18T+9amfhfjhBu74PykXA/+M4WU3yUnCmgTavEQZPEQ0h88z6vOY8PkLsakOYe02 24 | XwW45SNMqmaUx456bI7GorrgjUmVGkFyncu7uafppGShTyRdClKq2N/qAkojXUn3 25 | nmR3XSRGQ9D1gExziQzvnHyTMNOhxLxCWYmbREqvhRaH3zS/1UXKwXzZOq8DlGVB 26 | J7injtlPFx4up0BnmtcMUL10miO/47I77oLaYDGhigfQt1kVt8tD71KfdzTT3eqz 27 | ZoBTlNSe8PObdBtA4A8evzuJBbDrPyIlbX89h/2MkKyxuF7hbtFGw3Mg+ONjvuHf 28 | qg== 29 | -----END CERTIFICATE REQUEST----- 30 | -------------------------------------------------------------------------------- /vagrant/conf/ca_bundle/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /vagrant/conf/htpasswd: -------------------------------------------------------------------------------- 1 | duncan:$apr1$qHHEe5F3$1IvDBFCWlqDPK/L6Hj9zC0 2 | -------------------------------------------------------------------------------- /vagrant/conf/registry-config.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | loglevel: debug 3 | storage: 4 | filesystem: 5 | rootdirectory: /var/lib/docker-registry 6 | http: 7 | addr: 0.0.0.0:80 8 | auth: 9 | token: 10 | realm: https://garita.test.lan/v2/token 11 | service: registry.test.lan 12 | issuer: garita.test.lan 13 | rootcertbundle: /vagrant/vagrant/conf/ca_bundle/server.crt 14 | notifications: 15 | endpoints: 16 | - name: garita 17 | url: http://garita.test.lan/v2/webhooks/events 18 | timeout: 500ms 19 | threshold: 5 20 | backoff: 1s 21 | --------------------------------------------------------------------------------