You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by "nfrankel (via GitHub)" <gi...@apache.org> on 2023/03/02 14:48:59 UTC

[GitHub] [apisix-website] nfrankel commented on a diff in pull request #1509: blog: Add Make your security policy auditable post

nfrankel commented on code in PR #1509:
URL: https://github.com/apache/apisix-website/pull/1509#discussion_r1123218638


##########
blog/en/blog/2023/03/02/security-policy-auditable.md:
##########
@@ -0,0 +1,379 @@
+---
+title: Make your security policy auditable
+authors:
+  - name: Nicolas Fränkel
+    title: Author
+    url: https://github.com/nfrankel
+    image_url: https://avatars.githubusercontent.com/u/752258
+keywords:
+  - Cross-cutting concerns
+  - Architecture
+  - Security
+  - Spring Security
+  - Solutions Architecture
+description: >
+  Last week, I wrote about putting the right feature at the right place. I used rate limiting as an example, moving it from a library inside the application to the API Gateway. Today, I'll use another example: authentication and authorization.
+tags: [Ecosystem]
+image: https://blog.frankel.ch/assets/resources/security-policy-auditable/opa-horizontal-color.svg
+---
+
+>Last week, I wrote about [putting the right feature at the right place](https://blog.frankel.ch/right-feature-right-place/). I used rate limiting as an example, moving it from a library inside the application to the API Gateway. Today, I'll use another example: authentication and authorization.
+
+<!--truncate-->
+
+<head>
+    <link rel="canonical" href="https://blog.frankel.ch/security-policy-auditable/" />
+</head>
+
+## Securing a Spring Boot application
+
+I'll keep using Spring Boot in the following because I'm familiar with it. The Spring Boot application offers a REST endpoint to check employees' salaries.
+
+The specific use case is taken from the Open Policy Agent site (more later):
+
+>Create a policy that allows users to request their own salary as well as the salary of their direct subordinates.
+
+We need a way to:
+
+1. Authenticate an HTTP request as coming from a known user
+2. Check whether the user has access to the salary data
+
+In any other case, return a `401`.
+
+I'll pass an authentication token in the request to keep things simple. I won't rely on a dedicated authentication/authorization backend, such as Keycloak, but it should be a similar approach if you do.
+
+To enable Spring Security on the app, we need to add the Spring Boot Security Starter.
+
+```xml
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-security</artifactId>
+</dependency>
+```
+
+We also need to enable Spring Security to work its magic:
+
+```kotlin
+@SpringBootApplication
+@EnableWebSecurity
+class SecureBootApplication
+```
+
+With those two steps in place, we can start securing the application according to the above requirement:
+
+```kotlin
+internal fun security() = beans {                                       //1
+    bean {
+        val http = ref<HttpSecurity>()
+        http {
+            authorizeRequests {
+                authorize("/finance/salary/**", authenticated)          //2
+            }
+            addFilterBefore<UsernamePasswordAuthenticationFilter>(
+                TokenAuthenticationFilter(ref())                        //3
+            )
+            httpBasic { disable() }
+            csrf { disable() }
+            logout { disable() }
+            sessionManagement {
+                sessionCreationPolicy = SessionCreationPolicy.STATELESS
+            }
+        }
+        http.build()
+    }
+    bean { TokenAuthenticationManager(ref(), ref()) }                   //4
+}
+```
+
+1. Use the Kotlin Beans DSL - because I can
+2. Only allow access to the endpoint to authenticated users
+3. Add a filter in the filter chain to replace regular authentication
+4. Add a custom authentication manager
+
+Requests look like the following:
+
+```bash
+curl -H 'Authorization: xyz'  localhost:9080/finance/salary/bob
+```
+
+The filter extracts from the request the necessary data used to decide whether to allow the request or not:
+
+```kotlin
+internal class TokenAuthenticationFilter(authManager: AuthenticationManager) :
+    AbstractAuthenticationProcessingFilter("/finance/salary/**", authManager) {
+
+    override fun attemptAuthentication(req: HttpServletRequest, resp: HttpServletResponse): Authentication {
+        val header = req.getHeader("Authorization")                   //1
+        val path = req.servletPath.split('/')                         //2
+        val token = KeyToken(header, path)                            //3
+        return authenticationManager.authenticate(token)              //4
+    }
+
+    // override fun successfulAuthentication(
+}
+```
+
+1. Get the authentication token
+2. Get the path
+3. Wrap it under a dedicated structure
+4. Try to authenticate the token
+
+In turn, the manager tries to authenticate the token:
+
+```kotlin
+internal class TokenAuthenticationManager(
+    private val accountRepo: AccountRepository,
+    private val employeeRepo: EmployeeRepository
+) : AuthenticationManager {
+  override fun authenticate(authentication: Authentication): Authentication {
+    val token = authentication.credentials as String? ?:                       //1
+        throw BadCredentialsException("No token passed")
+    val account = accountRepo.findByPassword(token).orElse(null) ?:            //2
+        throw BadCredentialsException("Invalid token")
+    val path = authentication.details as List<String>
+    val accountId = account.id
+    val segment = path.last()
+    if (segment == accountId) return authentication.withPrincipal(accountId)   //3
+    val employee = employeeRepo.findById(segment).orElse(null)                 //4
+    val managerUserName = employee?.manager?.userName
+    if (managerUserName != null && managerUserName == accountId)               //5
+        return authentication.withPrincipal(accountId)                         //5
+    throw InsufficientAuthenticationException("Incorrect token")               //6
+  }
+}
+```
+
+1. Get the authorization token passed from the filter
+2. Try to find the account that has this token. For simplicity's sake, the token is stored in plain text without hashing
+3. If the account tries to access its data, allow it
+4. If not, we must load the hierarchy from another repo.
+5. If the account attempts to access data from an employee they manage, allow it.
+6. Else, deny it.
+
+The whole flow can be summarized as the following:
+
+![Spring Security Obfuscated Flow](http://www.plantuml.com/plantuml/svg/VL9DIyD04BtlhnXoKh6qYgVYHui6AuYLfj3pT3CbmNHtsGzh_VMwsOIbA-QGXFbuyzwRoSnOrDRj6uRSIWtEa0Oqa476k1HMomPGgJOrLofZ9LhSeY50pgKJreHI5yGwq5uryaWK6l8-oZnH_OcMMYxM4exkFSaKdlCrZ7UrGC5fRB11VHmVAXaXg1JpSde0huX_mDpPIkhw6sqjnMbpIQVOniARF0KyC0YsRqUZC5MJTLh0pUIAKME80NISlUSf5Fbh_hY62zWiybKEx_EYs2nNJt07Vbpax01XH628gI0kRUmrXemVDw0Fe5COSCk3WABTMy3zWxoUJ7mvOdk7yMf_BBxqvW2YmTZV5gBBD5_I02Ivvw8cZPfNnpFZjbANjK1BvX8Kskey4U1XAKLCXgKKSKgodC7r90iQEaBe5IMBN__sJw8gXk7th-gIO2UbtSelDli5k7tp0m00)
+
+Now, we can try some requests.
+
+```bash
+curl -H 'Authorization: bob' localhost:9080/finance/salary/bob
+```
+
+`bob` asks for his own salary, and it works.
+
+```bash
+curl -H 'Authorization: bob' localhost:9080/finance/salary/alice
+```
+
+`bob` asks for the salary of one of his subordinates, and it works as well.
+
+```bash
+curl -H 'Authorization: bob' localhost:9080/finance/salary/alice
+```
+
+`alice` asks for her manager's salary, which is not allowed.
+
+The code above works perfectly but has one big issue: there's no way to audit the logic. One must know Kotlin and how Spring Security works to ensure the implementation is sound.
+
+## Introducing Open Policy Agent
+
+Open Policy Agent, or OPA for short, describes itself as "Policy-based control for cloud native environments".
+
+>Stop using a different policy language, policy model, and policy API for every product and service you use. Use OPA for a unified toolset and framework for policy across the cloud native stack.
+>
+>Whether for one service or for all your services, use OPA to decouple policy from the service's code so you can release, analyze, and review policies (which security and compliance teams love) without sacrificing availability or performance.
+>
+>-- [OPA Website](https://www.openpolicyagent.org/)
+
+In short, OPA allows writing policies and offers a CLI and a daemon app to evaluate them.
+
+You write policies in a specific interpreted language named [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/), and I must admit it's not fun. Anyway, here's our above policy written in "clear" text:
+
+```rego
+package ch.frankel.blog.secureboot
+
+employees := data.hierarchy                                 //1
+
+default allow := false
+
+# Allow users to get their own salaries.
+allow {
+    input.path == ["finance", "salary", input.user]         //2
+}
+
+# Allow managers to get their subordinates' salaries.
+allow {
+    some username
+    input.path = ["finance", "salary", username]            //3
+    employees[input.user][_] == username                    //3

Review Comment:
   Good catch!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org