1
0

initial commit

Signed-off-by: dragonflylee <dragonflylee@outlook.com>
This commit is contained in:
2023-08-22 12:56:35 +08:00
commit 1a5dbfcd0d
8 changed files with 383 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
auth

26
README.md Normal file
View File

@@ -0,0 +1,26 @@
# Docker registry
```bash
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 -keyout auth.key -out auth.crt -subj "/CN=omnibus-gitlab-issuer"
docker run --restart always --network kind --name minio -v minio:/data \
-e MINIO_ROOT_USER=minio -e MINIO_ROOT_PASSWORD=minio123 \
-d minio/minio server --console-address :9090 /data
docker run --rm --network kind -v $HOME/.mc:/root/.mc -it minio/mc alias set s3 http://minio:9000 minio minio123
docker run --rm --network kind -v $HOME/.mc:/root/.mc -it minio/mc mb s3/registry
docker run --rm --network kind -v $HOME/.mc:/root/.mc -it minio/mc admin user add s3 registry registry123
docker run --rm --network kind -v $HOME/.mc:/root/.mc -it minio/mc admin policy attach s3 readwrite --user registry
docker run --restart always --network kind --name registry -v $PWD:/cert \
-e REGISTRY_AUTH=token -e REGISTRY_AUTH_TOKEN_REALM=http://172.18.0.1:5001/auth \
-e REGISTRY_AUTH_TOKEN_SERVICE=docker -e REGISTRY_AUTH_TOKEN_ISSUER=omnibus-gitlab-issuer \
-e REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/cert/auth.crt \
-p 0.0.0.0:5000:5000 --tmpfs /var/lib/registry -d registry:2
docker run --restart always --network kind --name registry --tmpfs /var/lib/registry \
-e REGISTRY_STORAGE=s3 -e REGISTRY_STORAGE_S3_ACCESSKEY=registry -e REGISTRY_STORAGE_S3_SECRETKEY=registry123 \
-e REGISTRY_STORAGE_S3_REGION=us-east-1 -e REGISTRY_STORAGE_S3_REGIONENDPOINT=http://minio:9000 \
-e REGISTRY_STORAGE_S3_BUCKET=registry -e REGISTRY_STORAGE_REDIRECT_DISABLE=true \
-p 0.0.0.0:5000:5000 -d registry:2
```

30
auth.crt Normal file
View File

@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFITCCAwmgAwIBAgIUCKn3oDHzPwNRwVciKAuyyMeboYMwDQYJKoZIhvcNAQEL
BQAwIDEeMBwGA1UEAwwVb21uaWJ1cy1naXRsYWItaXNzdWVyMB4XDTIzMDgyMjA1
NDcwNloXDTMzMDgxOTA1NDcwNlowIDEeMBwGA1UEAwwVb21uaWJ1cy1naXRsYWIt
aXNzdWVyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1MPpzW0v5dkt
YXHsh8MPqZlbQhpvOhe9n7iiDjEo3yaD6pxF1Gn/bEct4bjLG1KF1/zVZonmAoel
NqhT/lJwhcp06ML7z3e56wmk+AuSJ5KZiYeQWt9vHidI3UH7RzWHhb6hplBE7V9K
NWheGF4Rc7KFHKrQAK7Re+N8XuY1Z2MSHSG5c0xRxEpSUesVkesmU1hT0BToC9AI
dCOnoi33RByDBMeLf7nuKtCo5qIhBiTl1uusfDA6rGojtw085p0C32LfpzVPQGkD
lXzOp97iXP2Z3GpUuXXvOjBuxpzvUQDcWiPN8DqjWGCFgU6WrxP3a85oAyTnqnBS
SzTcD2VmHcA8SwhfWFgap4A872wRQ8AMVM+6y5LUoadJ/Cp03fy6WBZrqZXAsBBn
Sc3s18LAHuYEPSxQd/dpCbLwMzuUFnYgDUfezS42pFrJcg+l2+sgbKVBBlSKDvKj
8bz32RQ7HC28XACmi2uTMKhRDHJEoMljy4VqQ1M31h/KTHP2RfQQ0S/FW2LCIiVM
BRnzMbUoT+HC6IzPboRwf3LUUe+RRw7UXjJrLLAuoevs/OLFjNkRuVHdUtQQaWHF
ImpkKsiylWhtjrUjhV+fyPKXLnVH0KZS9svrqPoymmGgZmC3TmvQjX/gEf1DVrdx
oDcREztOc+cA2mNRBuvlBwDUOWrkWF8CAwEAAaNTMFEwHQYDVR0OBBYEFE32q/qx
88lPURQuhtu1VI3sxzK0MB8GA1UdIwQYMBaAFE32q/qx88lPURQuhtu1VI3sxzK0
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBABBLgiL0IKq2Jdt7
H6yWG70r4aLmgOFrnxwGZjRAVS+1pJh3MiWvlqyNOjaLwHX1WxtgAQm5dsVL9H1m
O1b1G5UrtP25192h/PzsgSP/qejSIfXAkHUPloeDzKPuJmkCNAkd+AGnVXqB1hF7
ngrUyCMt3Ed7gny94ImQf6DVhcq8IvsddoOq+2UFPx9fGe7Xd882NzSZbi6kEmsv
Yb28ovYi6Gsjp9o93VjTa9umtx2Kagn/+TNI5rK+bGp+lNLiTk7XkVt70pS37Fas
eIL4IZ6pwG/G7e1MCSJkGLuXODY5ebaMj3MYn6xbJA1KsozvFv6Hmy+s8upfWYGS
PndK/4UIcroy+zbuwPPlsoHorFiLCUW9+ThXIZ+062ahz+CX/2ETo8FpYToOhlDH
cHKddWtx50Hl0yrJdSiq/RmKWDtspHlY5fESjbdNQauuxSrNlG+k9pq2t+jc+Mk3
L1Yj1aFZIMie8NAmRLqC5XIhAKALqk0gPb0vpcvpZd1xs45OB/FO846z/oWnolsp
qgeRTxUTbpLlmsYYGB4VXTepakf0F4opPnrUoxVDbT6T3uDde9N9nWN/PemCs4Mv
8WNxs6WYi2/K5InVGrZygaLZVO4xThSWLq53cPDRZ2LPyRgRTIB8qHjc9M1X1MFY
UNAqtEZGKvCSxfZ1UfUnTBw6HJgV
-----END CERTIFICATE-----

52
auth.key Normal file
View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
s51eargpxc9q7tj4V1bt3njoJ86V
-----END PRIVATE KEY-----

8
go.mod Normal file
View File

@@ -0,0 +1,8 @@
module auth
go 1.16
require (
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gorilla/handlers v1.5.1
)

6
go.sum Normal file
View File

@@ -0,0 +1,6 @@
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=

90
server.go Normal file
View File

@@ -0,0 +1,90 @@
package main
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/gorilla/handlers"
)
var (
addr string
ti TokenIssuer
)
func main() {
flag.StringVar(&addr, "addr", ":5001", "address")
flag.StringVar(&ti.Issuer, "issuer", "omnibus-gitlab-issuer", "issuer name")
flag.StringVar(&ti.Audience, "audience", "docker", "audience name")
flag.Int64Var(&ti.Expiration, "expires", 900, "expiration")
flag.Parse()
log.SetFlags(log.LstdFlags | log.Lshortfile)
cert, err := tls.LoadX509KeyPair("auth.crt", "auth.key")
if err != nil {
log.Fatal(err)
}
ti.SigningKey = cert.PrivateKey
mux := http.NewServeMux()
mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) {
var resp struct {
Token string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
}
user, _, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
w.WriteHeader(http.StatusUnauthorized)
return
}
authRequest := ResolveScopeList(r.URL.Query().Get("scope"))
if len(authRequest) == 0 {
// Authentication-only request ("docker login"), pass through.
resp.Token, _ = ti.CreateJWT(user, authRequest)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&resp)
return
}
authResult, err := Authorized(user, authRequest)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
if resp.Token, err = ti.CreateJWT(user, authResult); err != nil {
log.Printf("CreateJWT %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
resp.ExpiresIn = ti.Expiration
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&resp)
})
h := handlers.CustomLoggingHandler(os.Stdout, mux, func(w io.Writer, p handlers.LogFormatterParams) {
fmt.Fprintf(w, "%s %s %d %s %d %s (%s)\n", p.TimeStamp.Format("2006/01/02 15:04:05"),
p.Request.Method, p.StatusCode, p.URL.RequestURI(), p.Size,
p.Request.RemoteAddr, time.Since(p.TimeStamp))
})
log.Fatal(http.ListenAndServe(addr, h))
}
func Authorized(user string, actions []ResourceActions) ([]ResourceActions, error) {
return actions, nil
}

170
token.go Normal file
View File

@@ -0,0 +1,170 @@
package main
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base32"
"encoding/base64"
"fmt"
"io"
"regexp"
"sort"
"strings"
"time"
"github.com/golang-jwt/jwt"
)
// Resource describes a resource by type and name.
type Resource struct {
Type string
Class string
Name string
}
// ResourceActions stores allowed actions on a named and typed resource.
type ResourceActions struct {
Type string `json:"type"`
Class string `json:"class,omitempty"`
Name string `json:"name"`
Actions []string `json:"actions"`
}
// ClaimSet describes the main section of a JSON Web Token.
type ClaimSet struct {
jwt.StandardClaims
// Private claims
Access []ResourceActions `json:"access"`
}
// ResolveScopeList converts a scope list from a token request's
// `scope` parameter into a list of standard access objects.
func ResolveScopeList(scopeList string) []ResourceActions {
scopeSpecs := strings.Split(scopeList, " ")
accessSet := make(map[Resource]map[string]bool, 2*len(scopeSpecs))
for _, scopeSpecifier := range scopeSpecs {
// There should be 3 parts, separated by a `:` character.
parts := strings.SplitN(scopeSpecifier, ":", 3)
if len(parts) != 3 {
continue
}
resourceType, resourceName, actions := parts[0], parts[1], parts[2]
resourceType, resourceClass := splitResourceClass(resourceType)
if resourceType == "" {
continue
}
requestedResource := Resource{
Type: resourceType,
Class: resourceClass,
Name: resourceName,
}
requestedAction, has := accessSet[requestedResource]
if !has {
requestedAction = make(map[string]bool, 2)
accessSet[requestedResource] = requestedAction
}
// Actions should be a comma-separated list of actions.
for _, action := range strings.Split(actions, ",") {
requestedAction[action] = true
}
}
requestedList := make([]ResourceActions, 0, len(accessSet))
for resource, actions := range accessSet {
ra := ResourceActions{
Name: resource.Name,
Class: resource.Class,
Type: resource.Type,
}
for action := range actions {
ra.Actions = append(ra.Actions, action)
}
sort.Strings(ra.Actions)
requestedList = append(requestedList, ra)
}
return requestedList
}
var typeRegexp = regexp.MustCompile(`^([a-z0-9]+)(\([a-z0-9]+\))?$`)
func splitResourceClass(t string) (string, string) {
matches := typeRegexp.FindStringSubmatch(t)
if len(matches) < 2 {
return "", ""
}
if len(matches) == 2 || len(matches[2]) < 2 {
return matches[1], ""
}
return matches[1], matches[2][1 : len(matches[2])-1]
}
// TokenIssuer represents an issuer capable of generating JWT tokens
type TokenIssuer struct {
Issuer string
Audience string
SigningKey crypto.PrivateKey
Expiration int64 // second
}
// CreateJWT creates and signs a JSON Web Token for the given subject and
// audience with the granted access.
func (issuer *TokenIssuer) CreateJWT(account string, grantedAccessList []ResourceActions) (string, error) {
randomBytes := make([]byte, 15)
if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil {
return "", err
}
randomID := base64.URLEncoding.EncodeToString(randomBytes)
now := time.Now().Unix()
token := ClaimSet{
StandardClaims: jwt.StandardClaims{
Subject: account,
Issuer: issuer.Issuer,
Audience: issuer.Audience,
ExpiresAt: now + issuer.Expiration,
NotBefore: now,
IssuedAt: now,
Id: randomID,
},
Access: grantedAccessList,
}
var jwtToken *jwt.Token
switch key := issuer.SigningKey.(type) {
case *rsa.PrivateKey:
jwtToken = jwt.NewWithClaims(jwt.SigningMethodRS256, token)
jwtToken.Header["kid"] = keyIDEncode(key.Public())
case *ecdsa.PrivateKey:
jwtToken = jwt.NewWithClaims(jwt.SigningMethodES256, token)
jwtToken.Header["kid"] = keyIDEncode(key.Public())
default:
return "", fmt.Errorf("unable to get PrivateKey %T", issuer.SigningKey)
}
return jwtToken.SignedString(issuer.SigningKey)
}
func keyIDEncode(pub crypto.PublicKey) string {
derBytes, _ := x509.MarshalPKIXPublicKey(pub)
sum := sha256.Sum256(derBytes)
s := strings.TrimRight(base32.StdEncoding.EncodeToString(sum[:30]), "=")
var buf bytes.Buffer
var i int
for i = 0; i < len(s)/4-1; i++ {
start := i * 4
end := start + 4
buf.WriteString(s[start:end] + ":")
}
buf.WriteString(s[i*4:])
return buf.String()
}