How we Integrated SPIFFE, OAuth2 and Spring Boot

Matthew Benedict Stocks
Wise Engineering
Published in
7 min readDec 14, 2022

At Wise the Security Engineering team supports the Security Squad by developing tools and building technical controls relevant to the security maturity of existing technological setup. We work closely with teams across Platform and Product to help improve our overall security posture and reduce the friction encountered when engineering new software at speed.

Photo by Jason Dent on Unsplash

The problem

As a company we have a goal of reducing the amount of time it takes to go from inception to creation with our services. We want teams across Wise to be able to focus on the challenges encountered in their domain, rather than cross-cutting concerns, such as how to secure their endpoints. At Wise our approach was to create a common shared library, allowing teams to share our security solution while still retaining their autonomy.

Standard security setups such as those seen with Spring Security often require a lot of boilerplate code (even if provided as part of a service template) which we wanted to reduce and where possible, abstract it entirely. Along with improving the quality of life for new and experienced engineers within Wise, this also helps to prevent any misconfiguration that could eventually lead to a vulnerability.

Another challenge which arose was to offer a security context that appears the same at the service layer, but can be created from any of the protocols used by our systems, as well as being extensible if necessary. We needed to allow developers to control access to services with minimal effort, alongside creating a security guardrail for exposed endpoints to ensure services have been configured correctly.

Introducing Wise Security

At Wise, our service mesh layer is handled by Envoy and the identity framework for secure communication between mesh nodes is SPIFFE/SPIRE. User based IAM is handled via the OIDC authorization protocol. Each authorization approach must be enforced at the service layer, but due to their nature are handled quite differently in the actual code. This is not necessarily an issue, but it can lead to unnecessary complexity and a potential for conflict. This can be a particular issue in a standard spring security filter chain. For example, where multiple authorizations may be present but only one provides the necessary fine grained information, potentially leading to false negative authorization results.

We consolidated the code for each authorization strategy into a single library, called wise-security, providing a paved road for all developers to follow, and a standard approach to applying access control. Wise-security can handle all of our authentication and authorization use cases, including operational tooling, public authentication, and S2S communication. It is as flexible as anything offered by out-of-the-box solutions such as Spring Security, yet it should be considered secure by default. It allows each auth type to co-exist without interference, and is built in such a way that errors in security configuration can be detected during unit test execution or when starting the application up in a test environment, preventing an insecure deployment.

Spring Security

Due to the nature and flexibility of Spring Security, it soon became clear that certain ambiguities in Spring annotations would not be suitable for what we wanted to achieve. If we want to be able to logically group access together (“roleA or “roleB” and “service foo” for example), this is not easily supported with out-of-the-box annotations. Yet, due to the nature of mixing S2S access with user access on the same endpoint, it is a behaviour we felt was crucial to achieve.

Rule-based authentication

To this end, initially we went with a custom set of security constructs that we refer to as Rules. A rule is essentially a condition that must be met for a request to be authorised to execute (more on this later). Rules themselves are grouped in policies using AND/OR logical constructs. More complex nesting can be used to achieve logical formulae. For example, conjunctive or disjunctive normal forms, or something more complex if necessary as shown in the following snippet.

AndRule.allOf(
Rule1,
Rule2
)

OrRule.anyOf(
Rule1,
AndRule.allOf(
Rule2,
Rule3
)
)

Security Policies

To prevent cluttered and often difficult-to-understand controller code — often found when large numbers of rules were required — we added a policy annotation which allows rules to be collected in a single location. Policies themselves take the form of a single Java class and are provided to a single annotation over an end point (or group of end points) that must be secured.

Service-to-Service Secure Communication

Our service mesh uses Envoy to proxy requests and SPIFFE as the identity framework.

An image showing two boxes representing k8s pods. Each pod has an envoy acting as a proxy and an agent issuing identity. Another box in each pod represents a service and they are using the envoys to communicate
Figure 1: Spiffe secured communication between containers

The overall process flow is quite standard in terms of how Envoy uses SPIRE (the SPIFFE run-time engine) as a Secret Discovery Service (SDS), in our case, used to obtain certificates to facilitate mTLS between K8s pods (see Figure 1).

The job of wise-security in this instance is to build a security context accessible from within the microservice that can be used for fine-grained authorization of service requests to both REST and gRPC APIs. To achieve this, services communicating via Envoy can identify themselves by service name via custom headers which are added (or overwritten to prevent spoofing) by the proxy. Service names can be used to build an individual OR Rule which can then be grouped into a policy.

public class SomeServicePolicy implements RulePolicy {
@Override
public Rule getRule() {
return OrRule.anyOf(
ServiceA,
ServiceB
)
}
}

The controller code itself is then annotated with the security policy either at the class or endpoint level. So taking the earlier example, if several services are grouped in a policy class with an OR rule, then a controller can be annotated as such:

@SecurityPolicy(SomeServicePolicy.class)
public class MyController {
// …
}

Using these simple constructs makes it simple to mix and match various authorizations versus creation of multiple enforcement points within the codebase as would be required with previous strategies.

OAuth2 and multi-tenancy

As mentioned previously, the library also needed to handle authorization from user interfaces which at Wise take the form of single page or native applications. The authorization protocol is OpenID Connect (OIDC — which mandates the use of JWT for identity and we use the same format for access tokens) and can be issued by multiple authorization servers depending on the use case. We achieved this via OAuth multitenancy, which in our implementation is separated using issuer claims embedded into the tokens (see Figure 2). This allowed us to develop plugin modules for each tenant and completely customise the token validation code per authorization server. The library can then load the required plugin depending on the issuer.

Two computer images representing applications that are communicating with an oblong representing a multi-tenant API. Each application has authenticated with a different authorization server. The API is shown select ing the authorization server to gain a key by reading a claim within the token
Figure 2: Overview of a multitenant authorization server

As with all JWT-based OAuth flows, the available authorizations (roles) for the authenticated user are cryptographically sealed into the token. This means that once validated using the JWKS, no further outside information is required by the server in order to make an authorization decision. Wise-security implementers can use these authorizations (or roles) to build new security rules and policies. As with the S2S case, an engineer can create logical groupings to give fine-grained access control. When mixing this with S2S rules, it is easy to see how it is possible to create a security policy that permits API access to Service A OR Any user with RoleB and RoleC, for example.

Integration with spring boot

As is common with libraries built for Spring Boot applications, a pattern of modular starter packs is provided with the library. Each starter pulls a set of modules that auto-configure Spring beans for inclusion in the application context depending on a set of user-defined options. A user may for example only require a configuration that supports gRPC security over REST, and may not require any S2S communication. It was always the intention that we would not pollute application contexts with unnecessary Java objects to keep the runtime state as lightweight as possible while still maintaining our core principle of secure by default.

However, while we are a company with a large amount of Spring Boot services, with Spring going from strength to strength as a platform, we were still careful to not include any Spring code within core modules. This is an implementation detail that works well when building Java libraries even if the foreseeable future is to target Spring Boot microservices, as it will reduce the pain somewhat if migration to a new platform is required or if there are major breaking changes introduced into Spring itself.

The additional benefit to keeping library code modular in this way is that additional components within your infrastructure (for example an API gateway that doesn’t use Spring) can still use core security functionality without the need to use the strict configuration designed for service code.

Conclusions

Our library provides an effective security guardrail with a number of clear benefits. Engineers within Wise can develop new services at speed without requiring any involvement from the security team. Application Security teams can track adoption of the new security control and can enforce a security posture without having to resort to manual review. And lastly, developing teams cannot ignore or bypass the security as it is baked into the templates that form the starting point for all services at Wise.

If you enjoyed reading this post and like the presented challenges, our Security Engineering team is hiring! Check out our open Engineering roles here.

--

--