Upgrade to Pro — share decks privately, control downloads, hide ads and more …

An Over-Engineering Disaster with Macaroons

An Over-Engineering Disaster with Macaroons

Macaroons are a decentralized authorization credential designed for distributed systems. They allow services to grant authorization tokens for one another, with precisely limited scope and ability. Macaroons may very well be the future of authorization across services.

Several months ago, my team eagerly adopted them, using an implementation in Go. Replacing our old system was easy enough—but working with macaroons turned out to be a disaster. In this talk, I share how our decision to use macaroons negatively affected our user experience, our own developer experience, and even our system availability. We also lost hours of developer time arguing about the difference between a “macaroon” and a “macaron”.

So, what makes macaroons so dang cool? How do they work? Why should gophers get excited about them and why did my team get excited about them? Why are macaroons such a tempting fit for a project, especially a Go project, and where did my team go wrong? What should a team of gophers consider and where did my team ultimately land?

This is a story about cool new technology, over-engineering, and coconut cookies.

Tess Rinearson

August 28, 2018
Tweet

More Decks by Tess Rinearson

Other Decks in Technology

Transcript

  1. // Authenticate returns the request with added tokens in the

    context func (a *API) Authenticate(req *http.Request) (*http.Request, error) { ctx := req.Context() user, pw, ok := req.BasicAuth() if !ok { return "", errors.New("no token") } err = a.checkTokenAuthn(ctx, user, pw) if err != nil { return "", errors.New("unauthenticated") } if user != "" { // if request was successfully authn’d, pass the user along ctx = newContextWithUser(ctx, user) } return req.WithContext(ctx), nil }
  2. // Authenticate returns the request with added tokens in the

    context func (a *API) Authenticate(req *http.Request) (*http.Request, error) { ctx := req.Context() user, pw, ok := req.BasicAuth() if !ok { return "", errors.New("no token") } err = a.checkTokenAuthn(ctx, user, pw) if err != nil { return "", errors.New("unauthenticated") } if user != "" { // if request was successfully authn’d, pass the user along ctx = newContextWithUser(ctx, user) } return req.WithContext(ctx), nil }
  3. // Authenticate returns the request with added tokens in the

    context func (a *API) Authenticate(req *http.Request) (*http.Request, error) { ctx := req.Context() user, pw, ok := req.BasicAuth() if !ok { return "", errors.New("no token") } err = a.checkTokenAuthn(ctx, user, pw) if err != nil { return "", errors.New("unauthenticated") } if user != "" { // if request was successfully authn’d, pass the user along ctx = newContextWithUser(ctx, user) } return req.WithContext(ctx), nil }
  4. // Authenticate returns the request with added tokens in the

    context func (a *API) Authenticate(req *http.Request) (*http.Request, error) { ctx := req.Context() user, pw, ok := req.BasicAuth() if !ok { return "", errors.New("no token") } err = a.checkTokenAuthn(ctx, user, pw) if err != nil { return "", errors.New("unauthenticated") } if user != "" { // if request was successfully authn’d, pass the user along ctx = newContextWithUser(ctx, user) } return req.WithContext(ctx), nil }
  5. 5 func (a *Authorizer) Authorize(req *http.Request) error { policies :=

    a.policyByRoute[req.RequestURI] if policies == nil { return errors.New("missing policy on this route") } grants, err := a.grantsByPolicies(policies) if err != nil { return errors.Wrap(err) } for _, g := range grants { if accessTokenData(g) == authn.Token(ctx) { return nil } } return ErrNotAuthorized }
  6. 5 func (a *Authorizer) Authorize(req *http.Request) error { policies :=

    a.policyByRoute[req.RequestURI] if policies == nil { return errors.New("missing policy on this route") } grants, err := a.grantsByPolicies(policies) if err != nil { return errors.Wrap(err) } for _, g := range grants { if accessTokenData(g) == authn.Token(ctx) { return nil } } return ErrNotAuthorized }
  7. 5 func (a *Authorizer) Authorize(req *http.Request) error { policies :=

    a.policyByRoute[req.RequestURI] if policies == nil { return errors.New("missing policy on this route") } grants, err := a.grantsByPolicies(policies) if err != nil { return errors.Wrap(err) } for _, g := range grants { if accessTokenData(g) == authn.Token(ctx) { return nil } } return ErrNotAuthorized }
  8. 5 func (a *Authorizer) Authorize(req *http.Request) error { policies :=

    a.policyByRoute[req.RequestURI] if policies == nil { return errors.New("missing policy on this route") } grants, err := a.grantsByPolicies(policies) if err != nil { return errors.Wrap(err) } for _, g := range grants { if accessTokenData(g) == authn.Token(ctx) { return nil } } return ErrNotAuthorized }
  9. “The ACL model is unable to make correct access decisions

    for interactions involving more than two principals, since required information is not retained across message sends.” 11 Tyler Close, ACLs don’t
  10. Basic Auth 17 func (r *Request) SetBasicAuth(username, password string) {

    r.Header.Set("Authorization", "Basic "+basicAuth(username, password)) } func basicAuth(username, password string) string { auth := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(auth)) } (from the net/http package)
  11. HMACs: keyed-Hash Message Authentication Codes 20 // compute hmac func

    GenerateMAC(message, key []byte) []byte { mac := hmac.New(sha256.New, key) mac.Write(message) return mac.Sum(nil) } // check hmac by recalculating server-side and then checking for equality func CheckMAC(message, messageMAC, key []byte) bool { expectedMAC := GenerateMac(message, key) return hmac.Equal(messageMAC, expectedMAC) }
  12. 22 func New(h func() hash.Hash, key []byte) hash.Hash { hm

    := new(hmac) hm.outer = h() hm.inner = h() hm.size = hm.inner.Size() hm.size = hm.inner.BlockSize() hm.ipad = make([]byte, hm.blocksize) hm.opad = make([]byte, hm.blocksize) if len(key) > hm.blocksize { // If key is too big, hash it. hm.outer.Write(key) key = hm.outer.Sum(nil) } copy(hm.ipad, key) copy(hm.opad, key) for i := range hm.ipad { hm.ipad[i] ^= 0x36 } for i := range hm.opad { hm.opad[i] ^= 0x5c } hm.inner.Write(hm.ipad) return hm } func (h *hmac) Sum(in []byte) []byte { origLen := len(in) in = h.inner.Sum(in) h.outer.Reset() h.outer.Write(h.opad) h.outer.Write(in[origLen:]) return h.outer.Sum(in[:origLen]) } (from the crypto/hmac package)
  13. 22 func New(h func() hash.Hash, key []byte) hash.Hash { hm

    := new(hmac) hm.outer = h() hm.inner = h() hm.size = hm.inner.Size() hm.size = hm.inner.BlockSize() hm.ipad = make([]byte, hm.blocksize) hm.opad = make([]byte, hm.blocksize) if len(key) > hm.blocksize { // If key is too big, hash it. hm.outer.Write(key) key = hm.outer.Sum(nil) } copy(hm.ipad, key) copy(hm.opad, key) for i := range hm.ipad { hm.ipad[i] ^= 0x36 } for i := range hm.opad { hm.opad[i] ^= 0x5c } hm.inner.Write(hm.ipad) return hm } func (h *hmac) Sum(in []byte) []byte { origLen := len(in) in = h.inner.Sum(in) h.outer.Reset() h.outer.Write(h.opad) h.outer.Write(in[origLen:]) return h.outer.Sum(in[:origLen]) } (from the crypto/hmac package)
  14. 26 func mintMacaroon(ctx context.Context, teamID string) (*macaroon.Macaroon, error) { secretKey

    := getSecretKeyFromParameterStore() // this is a fake function // Construct a unique identifier for the macaroon. id := &macaroons.Identifier{ Nonce: make([]byte, 16), KeyId: keyID, // random string IssuedAt: ptypes.TimestampNow(), } _, err := rand.Read(id.Nonce) idBytes, err := proto.Marshal(id) // Create an unattenuated macaroon for a team m, err := macaroon.New(secretKey, string(idBytes), "") // Attenuate the omnipotent macaroon with a caveat // limiting it to requests for the provided team ID. encodedCaveats, err := proto.Marshal(&ledgerpb.CaveatList{ Caveats: []*ledgerpb.Caveat{RequestIsForTeam: teamID}, }) err = m.AddFirstPartyCaveat(string(encodedCaveats)) return m, err }
  15. 26 func mintMacaroon(ctx context.Context, teamID string) (*macaroon.Macaroon, error) { secretKey

    := getSecretKeyFromParameterStore() // this is a fake function // Construct a unique identifier for the macaroon. id := &macaroons.Identifier{ Nonce: make([]byte, 16), KeyId: keyID, // random string IssuedAt: ptypes.TimestampNow(), } _, err := rand.Read(id.Nonce) idBytes, err := proto.Marshal(id) // Create an unattenuated macaroon for a team m, err := macaroon.New(secretKey, string(idBytes), "") // Attenuate the omnipotent macaroon with a caveat // limiting it to requests for the provided team ID. encodedCaveats, err := proto.Marshal(&ledgerpb.CaveatList{ Caveats: []*ledgerpb.Caveat{RequestIsForTeam: teamID}, }) err = m.AddFirstPartyCaveat(string(encodedCaveats)) return m, err }
  16. 26 func mintMacaroon(ctx context.Context, teamID string) (*macaroon.Macaroon, error) { secretKey

    := getSecretKeyFromParameterStore() // this is a fake function // Construct a unique identifier for the macaroon. id := &macaroons.Identifier{ Nonce: make([]byte, 16), KeyId: keyID, // random string IssuedAt: ptypes.TimestampNow(), } _, err := rand.Read(id.Nonce) idBytes, err := proto.Marshal(id) // Create an unattenuated macaroon for a team m, err := macaroon.New(secretKey, string(idBytes), "") // Attenuate the omnipotent macaroon with a caveat // limiting it to requests for the provided team ID. encodedCaveats, err := proto.Marshal(&ledgerpb.CaveatList{ Caveats: []*ledgerpb.Caveat{RequestIsForTeam: teamID}, }) err = m.AddFirstPartyCaveat(string(encodedCaveats)) return m, err }
  17. 26 func mintMacaroon(ctx context.Context, teamID string) (*macaroon.Macaroon, error) { secretKey

    := getSecretKeyFromParameterStore() // this is a fake function // Construct a unique identifier for the macaroon. id := &macaroons.Identifier{ Nonce: make([]byte, 16), KeyId: keyID, // random string IssuedAt: ptypes.TimestampNow(), } _, err := rand.Read(id.Nonce) idBytes, err := proto.Marshal(id) // Create an unattenuated macaroon for a team m, err := macaroon.New(secretKey, string(idBytes), "") // Attenuate the omnipotent macaroon with a caveat // limiting it to requests for the provided team ID. encodedCaveats, err := proto.Marshal(&ledgerpb.CaveatList{ Caveats: []*ledgerpb.Caveat{RequestIsForTeam: teamID}, }) err = m.AddFirstPartyCaveat(string(encodedCaveats)) return m, err }
  18. 27 type Macaroon struct { location string id []byte caveats

    []Caveat sig [hashLen]byte version Version } type Caveat struct { Id []byte VerificationId []byte Location string } Anatomy of a Macaroon (from https://github.com/go-macaroon/macaroon)
  19. 27 type Macaroon struct { location string id []byte caveats

    []Caveat sig [hashLen]byte version Version } type Caveat struct { Id []byte VerificationId []byte Location string } Anatomy of a Macaroon (from https://github.com/go-macaroon/macaroon)
  20. 27 type Macaroon struct { location string id []byte caveats

    []Caveat sig [hashLen]byte version Version } type Caveat struct { Id []byte VerificationId []byte Location string } Anatomy of a Macaroon (from https://github.com/go-macaroon/macaroon)
  21. 27 type Macaroon struct { location string id []byte caveats

    []Caveat sig [hashLen]byte version Version } type Caveat struct { Id []byte VerificationId []byte Location string } Anatomy of a Macaroon (from https://github.com/go-macaroon/macaroon)
  22. 27 type Macaroon struct { location string id []byte caveats

    []Caveat sig [hashLen]byte version Version } type Caveat struct { Id []byte VerificationId []byte Location string } Anatomy of a Macaroon (from https://github.com/go-macaroon/macaroon)
  23. // AddFirstPartyCaveat adds a caveat that will be verified //

    by the target service. func (m *Macaroon) AddFirstPartyCaveat(condition []byte) error { m.addCaveat(condition, nil, "") return nil } func (m *Macaroon) addCaveat(caveatId, verificationId []byte, loc string) error { m.appendCaveat(caveatId, verificationId, loc) m.sig = *keyedHash(&m.sig, caveatId) return nil } 28 Adding a Caveat (from https://github.com/go-macaroon/macaroon)
  24. // AddFirstPartyCaveat adds a caveat that will be verified //

    by the target service. func (m *Macaroon) AddFirstPartyCaveat(condition []byte) error { m.addCaveat(condition, nil, "") return nil } func (m *Macaroon) addCaveat(caveatId, verificationId []byte, loc string) error { m.appendCaveat(caveatId, verificationId, loc) m.sig = *keyedHash(&m.sig, caveatId) return nil } 28 Adding a Caveat (from https://github.com/go-macaroon/macaroon)
  25. 29 Adding a Caveat func keyedHash(key *[hashLen]byte, text []byte) *[hashLen]byte

    { h := keyedHasher(key) h.Write([]byte(text)) var sum [hashLen]byte hashSum(h, &sum) return &sum } func keyedHasher(key *[hashLen]byte) hash.Hash { return hmac.New(sha256.New, key[:]) } (from https://github.com/go-macaroon/macaroon)
  26. 29 Adding a Caveat func keyedHash(key *[hashLen]byte, text []byte) *[hashLen]byte

    { h := keyedHasher(key) h.Write([]byte(text)) var sum [hashLen]byte hashSum(h, &sum) return &sum } func keyedHasher(key *[hashLen]byte) hash.Hash { return hmac.New(sha256.New, key[:]) } (from https://github.com/go-macaroon/macaroon)
  27. Verifying a Macaroon 30 func authorizeRequest(teamID string, req *http.Request) error

    { ctx := req.Context() mac := authn.Macaroon(ctx) discharges := authn.DischargeMacaroons(ctx) if mac == nil { return httperror.ErrNotAuthenticated } verifier := caveatVerifier(teamID, req.URL.Path) err := mac.Verify(authzKey, verifier, discharges) if err != nil { return httperror.ErrNotAuthorized } return nil }
  28. Verifying a Macaroon 30 func authorizeRequest(teamID string, req *http.Request) error

    { ctx := req.Context() mac := authn.Macaroon(ctx) discharges := authn.DischargeMacaroons(ctx) if mac == nil { return httperror.ErrNotAuthenticated } verifier := caveatVerifier(teamID, req.URL.Path) err := mac.Verify(authzKey, verifier, discharges) if err != nil { return httperror.ErrNotAuthorized } return nil }
  29. Verifying a Macaroon 30 func authorizeRequest(teamID string, req *http.Request) error

    { ctx := req.Context() mac := authn.Macaroon(ctx) discharges := authn.DischargeMacaroons(ctx) if mac == nil { return httperror.ErrNotAuthenticated } verifier := caveatVerifier(teamID, req.URL.Path) err := mac.Verify(authzKey, verifier, discharges) if err != nil { return httperror.ErrNotAuthorized } return nil }
  30. Verifying a Macaroon 30 func authorizeRequest(teamID string, req *http.Request) error

    { ctx := req.Context() mac := authn.Macaroon(ctx) discharges := authn.DischargeMacaroons(ctx) if mac == nil { return httperror.ErrNotAuthenticated } verifier := caveatVerifier(teamID, req.URL.Path) err := mac.Verify(authzKey, verifier, discharges) if err != nil { return httperror.ErrNotAuthorized } return nil }
  31. 31 func caveatVerifier(teamID, path string) func(string) error { return func(caveatID

    string) error { var caveatList ledgerpb.CaveatList err := proto.Unmarshal([]byte(caveatID), &caveatList) if err != nil { return err } for _, caveat := range caveatList.Caveats { switch c := caveat.Caveat.(type) { case *ledgerpb.Caveat_RequestIsForTeam: reqIsForTeam := strings.ToLower(c.RequestIsForTeam) if strings.ToLower(teamID) != reqIsForTeam { return fmt.Errorf("credential scoped to team %q", reqIsForTeam) } case *ledgerpb.Caveat_NotBefore: ts, err := ptypes.Timestamp(c.NotBefore) if err != nil { return err } if time.Now().Before(ts) { return fmt.Errorf("credential not valid before %s", ts.Format(time.RFC3339)) } case *ledgerpb.Caveat_NotAfter: ts, err := ptypes.Timestamp(c.NotAfter) if err != nil { return err } if ts.Before(time.Now()) { return fmt.Errorf("credential expired at %s", ts.Format(time.RFC3339)) } case *ledgerpb.Caveat_OperationIsWithinCoarsePolicy: var n int var routePolicy ledgerpb.CoarsePolicy for pattern, pol := range minimumPolicyByRoute { if !pathMatch(pattern, path) { continue
  32. Third-party caveats 33 hmac(2,) 6 hmac(“perms:read-only”,6) 7 hmac([3rd party caveat],7)

    : type Caveat struct { Id []byte VerificationId []byte Location string }
  33. 36

  34. Macaroons: The Bad Parts 1. New availability dependency on Dashboard

    2. Difficult to use locally 3. Confusing behavior for users 43
  35. Macaroons: The Bad Parts 1. New availability dependency on Dashboard

    2. Difficult to use locally 3. Confusing behavior for users 4. Impossible to revoke immediately 44
  36. Macaroons: The Bad Parts 1. New availability dependency on Dashboard

    2. Difficult to use locally 3. Confusing behavior for users 4. Impossible to revoke immediately 5. Tricky format 45
  37. Macaroons: The Bad Parts 1. New availability dependency on Dashboard

    2. Difficult to use locally 3. Confusing behavior for users 4. Impossible to revoke immediately 5. Tricky format 6. Maybe misnamed? 46
  38. 47

  39. 48

  40. Macaroons: The Bad Parts 1. New availability dependency on Dashboard

    2. Difficult to use locally 3. Confusing behavior for users 4. Impossible to revoke immediately 5. Tricky format 6. Maybe misnamed? 49
  41. 50

  42. Further reading and watching • ACLs don’t http://waterken.sourceforge.net/aclsdont/ • Macaroons

    paper https://ai.google/research/pubs/pub41892 • Tony Arcieri’s talk on Macaroons https://www.youtube.com/watch?v=bFn-wjQtxZ0 • Ulfar Erlingsson, Macaroons co-author https://air.mozilla.org/macaroons-cookies-with- contextual-caveats-for-decentralized-authorization-in-the-cloud/ • Dan McKinley, Choose Boring Technology http://mcfunley.com/choose-boring-technology • Macarons images from https://www.danasbakery.com/collections/flavors 51 [email protected] // @_tessr