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
}

我认为就是一些动作操错等信息,然后再验证操作是否合法然后才能算通过验证。
唔,好麻烦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容