Secure access control is a critical aspect of any successful platform or service. Unfortunately, data breaches have been increasing at an alarming rate over the last few years. While there is more attention than ever before on external threats and securing public attack surfaces, it’s essential to apply this same level of scrutiny to internal services. In the event that a bad actor does gain access to part of your system, approaches like the Zero Trust paradigm can mitigate their ability to traverse internal services. We must work hard to not only eliminate vulnerabilities, but also to minimize the blast radius of any single vulnerability.
Here at Pinwheel, we take pride in having the most secure payroll data connectivity API in the industry with an "A" grade Security Scorecard and certifications including ISO 27001. We are constantly looking for areas to level up our security posture. This includes projects this year focused on improving authn/z, access control, and auditing across our internal systems via standard frameworks and libraries. This blog post shares resources and suggestions that may help organizations improve their access control across internal services.
Background: The Cloud, OWASP, OAuth, and the evolving landscape of App Sec
Building secure services requires knowledge and skills across a variety of complex technical areas. Luckily, there are numerous resources available to support these efforts. Cloud computing providers often provide robust support around infrastructure and network security (e.g., shared responsibility models for AWS and GCP). They also tend to provide suites of security tooling that are straightforward to configure.
Regardless of infrastructure provider, developers are responsible for application level security. Organizations like the OWASP Foundation provide helpful resources to understand and address common application security threats. At the top of the latest OWASP Top 10 Web Application Security Risks is Broken Access Control, often resulting in unauthorized access to sensitive API resources. In the context of internal services, Broken Access Control can be the difference between a small and quickly contained security incident or a system-wide data breach that may take weeks or months to notice and remediate.
At the heart of access control is the mechanism or framework by which services can authenticate (authn) and authorize (authz) access throughout the system. OAuth2 is a robust framework for authn/z across a variety of scenarios, with supported Grant Types for different auth scenarios. For now, we’ll be focusing on the Client Credentials Grant that can be effective for internal service-to-service auth flows. OAuth2 also supports grant types and mechanisms for securing human-to-service flows that are essential for comprehensive access control.
Example Scenario with OAuth2 Solution
Our goal is to allow our internal documents-service to securely allow access from our internal trusty-service. There are a number of other internal services across the system, including the utility-service that should not have access to documents-service.
Let's show how we can accomplish this goal using the OAuth2 Client Credentials Grant flow per the following diagram:
Generating Access Tokens via OAuth2 Service
First, we need an OAuth2 service to authenticate trusty-service’s credentials and issue access tokens that can be used to communicate with documents-service. There are a variety of vendors (eg. Auth0) and open source projects (eg. Hydra or Keycloak) that provide OAuth2 implementations (see RFC 6749). For this example scenario we’ll be writing an overly-simplified OAuth2 service using Python’s FastAPI framework that only supports the Client Credentials Grant.
We assume the authenticate_client() function will securely validate the client’s credentials (argon2 or bcrypt can help) and the is_client_allowed_scope() function will reject clients requesting access to a scope they are not allow-listed to use.
What about the generate_access_token() function? We can create a JSON Web Token (JWT) or an opaque token that the client can use as temporary proof of identity. Every system and use case is different, for this scenario we will proceed to issue JWT access tokens that can be parsed and validated downstream without depending on extra requests to the OAuth2 service for every internal service request (higher availability + lower latency across the system).
JWTs provide cryptographically signed, not encrypted, JSON payloads with “claims” about the owner’s identity. While JWT signatures prevent tampering, the claims are visible to anyone and should never contain sensitive information. RFC 7523 and the PyJWT library can help us create our JWT access tokens.
We assume that get_signing_key() will return the key ID + private key of one of our valid asymmetric encryption (signing) keys. One important feature that we’re omitting in this example OAuth2 service is an endpoint for other services to fetch the key ID + public key for all valid signing keys. This is necessary for downstream services to validate JWT signatures and ensure they were created by the OAuth2 service. Please see RFC 7517 for more information on JSON Web Keys (JWKs).
Using Access Tokens as a Client
Now, we need trusty-service to fetch access tokens and pass them along in requests to documents-service. We’ll assume that we have a token_cache that will securely cache non-expired access tokens and use the requests library for our https requests.
Enforcing Access Control on Access Tokens
Finally, the documents service needs to validate access tokens and authorize access based on the authenticated identity claims. This can often be performed as part of API middleware or similar mechanisms such as FastAPI Dependencies.
The PyJWT library includes helpful features to validate standard claims, as specified in the JWT_REQUIREMENTS constant. We assume that jwk_cache provides the valid JWKs from our OAuth2 service mentioned above and that is_client_allowed_resource_action() applies business logic to determine if the authenticated client is allowed to proceed with their request.
Access Control Benefits and Additional Considerations
The OAuth2 flow shown above provides access control via short lived, cryptographically signed, specifically scoped tokens in a manner that should introduce minimal latency to our system. If a bad actor were to compromise the utility-service, they would have no way to access the documents-service. If a bad actor were to compromise an access token from the trusty-service, they would have a short window to perform actions allowed by the specific scope of the token.
This access control flow centralizes authentication of client credentials behind the OAuth2 service to reduce responsibility of downstream services and exposure of credentials. This also provides opportunity for streamlined (graceful) rotation of credentials without updates to downstream services.
Some opportunities to strengthen this access control flow include:
- Regularly rotating client credentials and JWKs
- Logging + events (eg. SIEM) for authn/z attempts by both the OAuth2 service and downstream services (eg. documents-service) to maintain an audit trail and monitor for anomalous activity
- Deny-list mechanisms for compromised JWTs (eg. using jti claim)
- Injecting custom claims into access tokens for Attribute Based Access Control, in general ensuring fine-grained access policies that are secure (deny) by default
- Regularly re-evaluating design choices as newly vetted and well supported options become available (eg. using PASETOs instead of JWTs or upgrading to more secure signing algorithms)
- Standardizing authorization policies and decision-making across the system with tools such as Open Policy Agent (OPA)
That's it for now! Thanks for reading through some of these shared resources and suggestions that may help your organization improve their access control across internal services.
Check out our related content down below to see other tech spotlights!