docker registry token验证分析
docker registry通过htpasswd,silly,token等多种方式进行安全认证,在这里我用的是token这种方式。
registry配置文件如下:
auth:
token:
issuer: dtest
realm: http://ip:5013/registry/v1/token
rootcertbundle: /etc/registry/root.crt
service: token-service
可以通过调用api服务的方式来进行token验证。
当配置好以后执行docker login ip:5000进行登录的时候无法登录成功并且给返回authorization server did not include a token in the response
错误信息,查看registry 日志发现有这样一条错误日志
WARN[0024] error authorizing context: authorization token required
通过各种方式尝试,并且查资料都没有搞明白什么意思,无奈只能自己编译registry代码自己搞,具体编译过程这里不再赘述https://github.com/docker/distribution
。
通过错误日志定位发现在registry/handlers/app.go
文件里面有这样一段代码:
func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for headerName, headerValues := range app.Config.HTTP.Headers {
for _, value := range headerValues {
w.Header().Add(headerName, value)
}
}
context := app.context(w, r)
if err := app.authorized(w, r, context); err != nil {
ctxu.GetLogger(context).Warnf("error authorizing context: %v", err)
return
}
可以看到错误是在最下面返回的,既然如此那么出问题的地方已经定位,就看看app.authorized(w, r, context)
这个里面究竟是什么鬼东西。
func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Context) error {
ctxu.GetLogger(context).Debug("authorizing request")
repo := getName(context)
if app.accessController == nil {
return nil // access controller is not enabled.
}
var accessRecords []auth.Access
if repo != "" {
accessRecords = appendAccessRecords(accessRecords, r.Method, repo)
if fromRepo := r.FormValue("from"); fromRepo != "" {
// mounting a blob from one repository to another requires pull (GET)
// access to the source repository.
accessRecords = appendAccessRecords(accessRecords, "GET", fromRepo)
}
} else {
// Only allow the name not to be set on the base route.
if app.nameRequired(r) {
// For this to be properly secured, repo must always be set for a
// resource that may make a modification. The only condition under
// which name is not set and we still allow access is when the
// base route is accessed. This section prevents us from making
// that mistake elsewhere in the code, allowing any operation to
// proceed.
if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized); err != nil {
ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors)
}
return fmt.Errorf("forbidden: no repository name")
}
accessRecords = appendCatalogAccessRecord(accessRecords, r)
}
ctx, err := app.accessController.Authorized(context.Context, accessRecords...)
if err != nil {
switch err := err.(type) {
case auth.Challenge:
// Add the appropriate WWW-Auth header
err.SetHeaders(w)
if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(accessRecords)); err != nil {
ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors)
}
default:
// This condition is a potential security problem either in
// the configuration or whatever is backing the access
// controller. Just return a bad request with no information
// to avoid exposure. The request should not proceed.
ctxu.GetLogger(context).Errorf("error checking authorization: %v", err)
w.WriteHeader(http.StatusBadRequest)
}
return err
}
这段代码还挺长,不过看了一下大概是`app.accessController.Authorized(`执行这个验证方法的时候出了问题,然后我就在这边打印了一下错误日志,发现果然是这个地方出的问题,然后我大概看了一下这个方法的实现:
registry/auth/htpasswd/access.go:func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) {
registry/auth/silly/access.go:func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) {
registry/auth/token/accesscontroller.go:func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) {
看样子是根据不同的方式实现了不同的验证逻辑,不过不用担心我这里用的是token验证,肯定就是最下面那个方法实现的,
func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) {
challenge := &authChallenge{
realm: ac.realm,
service: ac.service,
accessSet: newAccessSet(accessItems...),
}
req, err := context.GetRequest(ctx)
if err != nil {
return nil, err
}
parts := strings.Split(req.Header.Get("Authorization"), " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
challenge.err = ErrTokenRequired
return nil, challenge
}
终于看到 原来我的返回的token没有符合格式要求。
当程序拿到token字符串以后会根据空格分割然后拿到第二部分字符串根据字符串生成一个Token对象:
func NewToken(rawToken string) (*Token, error) {
parts := strings.Split(rawToken, TokenSeparator)
if len(parts) != 3 {
return nil, ErrMalformedToken
}
var (
rawHeader, rawClaims = parts[0], parts[1]
headerJSON, claimsJSON []byte
err error
)
defer func() {
if err != nil {
log.Infof("error while unmarshalling raw token: %s", err)
}
}()
if headerJSON, err = joseBase64UrlDecode(rawHeader); err != nil {
err = fmt.Errorf("unable to decode header: %s", err)
return nil, ErrMalformedToken
}
if claimsJSON, err = joseBase64UrlDecode(rawClaims); err != nil {
err = fmt.Errorf("unable to decode claims: %s", err)
return nil, ErrMalformedToken
}
token := new(Token)
token.Header = new(Header)
token.Claims = new(ClaimSet)
token.Raw = strings.Join(parts[:2], TokenSeparator)
if token.Signature, err = joseBase64UrlDecode(parts[2]); err != nil {
err = fmt.Errorf("unable to decode signature: %s", err)
return nil, ErrMalformedToken
}
if err = json.Unmarshal(headerJSON, token.Header); err != nil {
return nil, ErrMalformedToken
}
if err = json.Unmarshal(claimsJSON, token.Claims); err != nil {
return nil, ErrMalformedToken
}
return token, nil
}
Token:
type ClaimSet struct {
// Public claims
Issuer string `json:"iss"`
Subject string `json:"sub"`
Audience string `json:"aud"`
Expiration int64 `json:"exp"`
NotBefore int64 `json:"nbf"`
IssuedAt int64 `json:"iat"`
JWTID string `json:"jti"`
// Private claims
Access []*ResourceActions `json:"access"`
}
// Header describes the header section of a JSON Web Token.
type Header struct {
Type string `json:"typ"`
SigningAlg string `json:"alg"`
KeyID string `json:"kid,omitempty"`
X5c []string `json:"x5c,omitempty"`
RawJWK *json.RawMessage `json:"jwk,omitempty"`
}
// Token describes a JSON Web Token.
type Token struct {
Raw string
Header *Header
Claims *ClaimSet
Signature []byte
}
转换成功以后然后根据更详细的规则做token验证:
func (t *Token) Verify(verifyOpts VerifyOptions) error {
// Verify that the Issuer claim is a trusted authority.
if !contains(verifyOpts.TrustedIssuers, t.Claims.Issuer) {
log.Infof("token from untrusted issuer: %q", t.Claims.Issuer)
return ErrInvalidToken
}
// Verify that the Audience claim is allowed.
log.Info("||||||||||||,", verifyOpts.AcceptedAudiences, t.Claims.Audience)
if !contains(verifyOpts.AcceptedAudiences, t.Claims.Audience) {
log.Infof("token intended for another audience: %q", t.Claims.Audience)
return ErrInvalidToken
}
// Verify that the token is currently usable and not expired.
currentTime := time.Now()
ExpWithLeeway := time.Unix(t.Claims.Expiration, 0).Add(Leeway)
if currentTime.After(ExpWithLeeway) {
log.Infof("token not to be used after %s - currently %s", ExpWithLeeway, currentTime)
return ErrInvalidToken
}
NotBeforeWithLeeway := time.Unix(t.Claims.NotBefore, 0).Add(-Leeway)
if currentTime.Before(NotBeforeWithLeeway) {
log.Infof("token not to be used before %s - currently %s", NotBeforeWithLeeway, currentTime)
return ErrInvalidToken
}
// Verify the token signature.
if len(t.Signature) == 0 {
log.Info("token has no signature")
return ErrInvalidToken
}
// Verify that the signing key is trusted.
signingKey, err := t.VerifySigningKey(verifyOpts)
if err != nil {
log.Info(err)
return ErrInvalidToken
}
// Finally, verify the signature of the token using the key which signed it.
if err := signingKey.Verify(strings.NewReader(t.Raw), t.Header.SigningAlg, t.Signature); err != nil {
log.Infof("unable to verify token signature: %s", err)
return ErrInvalidToken
}
return nil
}
然后根据Token生成访问信息:
func (t *Token) accessSet() accessSet {
if t.Claims == nil {
return nil
}
accessSet := make(accessSet, len(t.Claims.Access))
for _, resourceActions := range t.Claims.Access {
resource := auth.Resource{
Type: resourceActions.Type,
Name: resourceActions.Name,
}
set, exists := accessSet[resource]
if !exists {
set = newActionSet()
accessSet[resource] = set
}
for _, action := range resourceActions.Actions {
set.add(action)
}
}
return accessSet
}
我认为就是一些动作操错等信息,然后再验证操作是否合法然后才能算通过验证。
唔,好麻烦。