-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] authenticated microservices #1238
Draft
petertrr
wants to merge
90
commits into
master
Choose a base branch
from
feature/authenticated-microservices
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+525
−16
Draft
Changes from 72 commits
Commits
Show all changes
90 commits
Select commit
Hold shift + click to select a range
8009db2
Improvements for Gradle build
petertrr d75fe3c
Switch `spring-cloud-kubernetes` implementation from `kubernetes-clie…
petertrr 8796ed7
[skip ci] [WIP] App-to-app authentication using ServiceAccount tokens
petertrr 3c4d60a
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 90c27ca
[skip ci] WIP: authenticated microservices
petertrr b501eda
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 12f01d9
[skip ci] Extract sa-token mount into _helpers.tpl; fix path to the m…
petertrr bb7f1de
Read ConfigMap using Kubernetes fabric8 client
petertrr 8e27f7d
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 6121181
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 2b0900d
[skip ci] Extract the WebClientCustomizer into save-cloud-common, plu…
petertrr 415c4f6
[skip ci] Cleanup
petertrr 1f0e8f8
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 09df217
[Helm] Set another port as a management port for all JVM services
petertrr c235283
[skip ci] WIP
petertrr 5c12d11
Enable `@ConditionalOnCloudPlatform` for web client customizer bean
petertrr 49a25a3
Handle error status code when uploading test suite source snapshot fr…
petertrr 1de5b2f
Disable CSRF so that everything works
petertrr 86dd0a9
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr e390516
[skip ci] Move KubernetesAuthenticationUtils to save-cloud-common
petertrr a612f55
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 148e053
[skip ci]
petertrr f9c0984
Minor refactoring of WebClientCustomizers
petertrr 6f33194
Extension method for KubernetesAuthenticationUtils
petertrr 23bead9
Code style
petertrr bc037cc
Merge branch 'master' into feature/authenticated-microservices
petertrr 6b048b6
Apply `WebClientCustomizers` to all `WebClient`s; fix `!kubernetes` s…
petertrr bdf937a
Protect orchestrator and sandbox with SA Token Authorization
petertrr 698e2a5
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 2579328
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 20c7171
Code style
petertrr fdac9db
Merge branch 'master' into feature/authenticated-microservices
petertrr b69b3e4
Merge branch 'master' into feature/authenticated-microservices
petertrr 39849e7
[skip ci] Fix bean name clash
petertrr c288726
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr e53392d
[skip ci] WIP: Restore security-related beans in a configuration class
petertrr 2a6fd31
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 7a04db0
[skip ci] Cleanup after merge
petertrr 15ae7e3
[skip ci] Resolve conflict between beans
petertrr 92c4e01
[skip ci] Disable spring-security autoconfiguration on preprocessor; …
petertrr cd60fe4
[skip ci] Cleanup
petertrr b20dda2
Code style
petertrr b9959bc
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr eca3464
Fix compilation
petertrr 45a0c3b
Fix compilation
petertrr 4050086
Merge branch 'master' into feature/authenticated-microservices
petertrr 3c1ff88
`@Component` -> `@Configuration`; remove duplicated imports
petertrr ec79fd3
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr fd0d57a
Increase logging level; minor refactoring
petertrr 7a25619
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr fee4a56
Fix typo
petertrr 0e2ee9b
Use `securityMatcher`s to glue together multiple security chains
petertrr c1a97c0
Import `WebClientCustomizers` in backend
petertrr a2da43b
Bind `orchestrator-sa` ServiceAccount to `microservice` ClusterRole
petertrr d7c18a8
Improved logging
petertrr 105231c
Different path matcher for k8s-secured endpoints
petertrr 7a390ea
Helm: mount SA token to orchestrator; add missing property in backend…
petertrr d5581c8
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 454d67b
Merge branch 'master' into feature/authenticated-microservices
petertrr 878822d
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 4844e9b
Move k8s-security-related classes to authService module
petertrr ff458a2
Code style, minor fixes
petertrr b42254b
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr b5e581c
Code style
petertrr 9566b3a
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 6a3584c
Try to build w/o caches
petertrr e3b9d9c
One more experiment to fix Kapt error
petertrr 2afa564
One more experiment to fix Kapt error
petertrr aa06a2c
One more experiment to fix Kapt error
petertrr 4ae4edd
One more experiment to fix Kapt error
petertrr ac57556
Remove most of JPA related depdencies from auth service; revert chang…
petertrr 9c3d918
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 804dffa
Follow-ups after merge
petertrr dce76ae
Update helm_push.yml
petertrr 467483c
Update helm_push.yml
petertrr f1265ff
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 64bd7df
Fixme until #1247
petertrr bbd1b34
Eclude security autoconfuguration from preprocessor
petertrr 64ac8f2
Merge remote-tracking branch 'origin/feature/authenticated-microservi…
petertrr 2e24b08
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr bc4fb35
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr 5813501
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr df910bb
Add more endpoints to exclusion list
petertrr 11a89f5
Merge remote-tracking branch 'origin/master' into feature/authenticat…
petertrr d9f1f31
Merge branch 'master' into feature/authenticated-microservices
sanyavertolet ee6e5a1
[skip ci] updated branch
sanyavertolet 5c3c681
[skip ci] added SA to demo and gateway
sanyavertolet 925d062
[skip ci] wrapper tokenewviews
sanyavertolet fb80354
Merge branch 'master' into feature/authenticated-microservices
sanyavertolet 76ce335
Merge branch 'master' into feature/authenticated-microservices
nulls File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
...e/src/main/kotlin/com/saveourtool/save/authservice/config/SecurityWebClientCustomizers.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* Customization for spring `WebClient` | ||
*/ | ||
|
||
package com.saveourtool.save.authservice.config | ||
|
||
import com.saveourtool.save.authservice.utils.SA_HEADER_NAME | ||
import com.saveourtool.save.utils.debug | ||
import com.saveourtool.save.utils.getLogger | ||
import org.springframework.beans.factory.annotation.Value | ||
|
||
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform | ||
import org.springframework.boot.cloud.CloudPlatform | ||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.web.reactive.function.client.ClientRequest | ||
import org.springframework.web.reactive.function.client.WebClient | ||
|
||
import java.nio.file.Path | ||
import java.time.Duration | ||
import java.util.concurrent.atomic.AtomicLong | ||
import java.util.concurrent.atomic.AtomicReference | ||
|
||
import kotlin.io.path.readText | ||
|
||
/** | ||
* A configuration class that can be used to import all related [WebClientCustomizer] beans. | ||
*/ | ||
@Configuration | ||
open class SecurityWebClientCustomizers { | ||
@Bean | ||
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) | ||
@Suppress("MISSING_KDOC_ON_FUNCTION", "MISSING_KDOC_CLASS_ELEMENTS") | ||
open fun serviceAccountTokenHeaderWebClientCustomizer( | ||
@Value("\${com.saveourtool.cloud.kubernetes.sa-token.expiration.minutes:5}") expirationTimeMinutes: Long | ||
) = ServiceAccountTokenHeaderWebClientCustomizer(expirationTimeMinutes) | ||
} | ||
|
||
/** | ||
* A [WebClientCustomizer] that appends Kubernetes' ServiceAccount token as a custom header. | ||
* | ||
* @param expirationTimeMinutes for how long token should be reused from memory before reading it from the file again. | ||
*/ | ||
class ServiceAccountTokenHeaderWebClientCustomizer( | ||
expirationTimeMinutes: Long, | ||
) : WebClientCustomizer { | ||
@Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") | ||
private val logger = getLogger<ServiceAccountTokenHeaderWebClientCustomizer>() | ||
private val wrapper = ExpiringValueWrapper(Duration.ofMinutes(expirationTimeMinutes)) { | ||
val token = Path.of("/var/run/secrets/tokens/service-account-projected-token").readText() | ||
token | ||
} | ||
|
||
override fun customize(builder: WebClient.Builder) { | ||
builder.filter { request, next -> | ||
val token = wrapper.getValue() | ||
logger.debug { "Appending `$SA_HEADER_NAME` header to the request ${request.method()} to ${request.url()}" } | ||
ClientRequest.from(request) | ||
.header(SA_HEADER_NAME, token) | ||
.build() | ||
.let(next::exchange) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* A wrapper around a value of type [T] that caches it for [expirationTimeMillis] and then recalculates | ||
* using [valueGetter] | ||
* | ||
* @param expirationTime value expiration time | ||
* @property valueGetter a function to calculate the value of type [T] | ||
*/ | ||
class ExpiringValueWrapper<T : Any>( | ||
expirationTime: Duration, | ||
private val valueGetter: () -> T, | ||
) { | ||
private val expirationTimeMillis = expirationTime.toMillis() | ||
private val lastUpdateTimeMillis = AtomicLong(0) | ||
private val value: AtomicReference<T> = AtomicReference() | ||
|
||
/** | ||
* @return cached value or refreshes the value and returns it | ||
*/ | ||
fun getValue(): T { | ||
val current = System.currentTimeMillis() | ||
if (current - lastUpdateTimeMillis.get() > expirationTimeMillis) { | ||
value.lazySet(valueGetter()) | ||
lastUpdateTimeMillis.lazySet(current) | ||
} | ||
return value.get() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
208 changes: 208 additions & 0 deletions
208
...e/src/main/kotlin/com/saveourtool/save/authservice/utils/KubernetesAuthenticationUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
/** | ||
* Utilities to configure Kubernetes ServiceAccount token-based authentication in Spring Security. | ||
*/ | ||
|
||
package com.saveourtool.save.authservice.utils | ||
|
||
import com.saveourtool.save.utils.debug | ||
import com.saveourtool.save.utils.getLogger | ||
|
||
import io.fabric8.kubernetes.api.model.authentication.TokenReview | ||
import io.fabric8.kubernetes.client.KubernetesClient | ||
import io.fabric8.kubernetes.client.utils.Serialization | ||
import org.intellij.lang.annotations.Language | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform | ||
import org.springframework.boot.cloud.CloudPlatform | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.context.annotation.Import | ||
import org.springframework.core.annotation.Order | ||
import org.springframework.http.HttpStatus | ||
import org.springframework.security.authentication.BadCredentialsException | ||
import org.springframework.security.authentication.ReactiveAuthenticationManager | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken | ||
import org.springframework.security.config.web.server.SecurityWebFiltersOrder | ||
import org.springframework.security.config.web.server.ServerHttpSecurity | ||
import org.springframework.security.core.Authentication | ||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken | ||
import org.springframework.security.web.server.SecurityWebFilterChain | ||
import org.springframework.security.web.server.authentication.AuthenticationWebFilter | ||
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint | ||
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter | ||
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher | ||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers | ||
import org.springframework.stereotype.Component | ||
import org.springframework.web.server.ServerWebExchange | ||
import reactor.core.publisher.Mono | ||
import reactor.kotlin.core.publisher.switchIfEmpty | ||
import reactor.kotlin.core.publisher.toMono | ||
|
||
const val SA_HEADER_NAME = "X-Service-Account-Token" | ||
|
||
/** | ||
* A Configuration class that can be used to import all related beans to set up Spring Security | ||
* to work with Kubernetes ServiceAccount tokens. | ||
*/ | ||
@Configuration | ||
@Import( | ||
ServiceAccountTokenExtractorConverter::class, | ||
ServiceAccountAuthenticatingManager::class, | ||
) | ||
@Suppress( | ||
"AVOID_USING_UTILITY_CLASS", // Spring beans need to be declared inside `@Configuration` class. | ||
) | ||
class KubernetesAuthenticationUtils { | ||
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) | ||
@Bean | ||
@Order(2) | ||
@Suppress( | ||
"MISSING_KDOC_CLASS_ELEMENTS", | ||
"MISSING_KDOC_ON_FUNCTION", | ||
) | ||
fun internalSecuredSecurityChain( | ||
http: ServerHttpSecurity, | ||
serviceAccountAuthenticatingManager: ServiceAccountAuthenticatingManager, | ||
serviceAccountTokenExtractorConverter: ServiceAccountTokenExtractorConverter, | ||
): SecurityWebFilterChain = http.run { | ||
securityMatcher( | ||
NegatedServerWebExchangeMatcher( | ||
ServerWebExchangeMatchers.pathMatchers("/api/**", "/sandbox/api/**") | ||
) | ||
) | ||
.authorizeExchange() | ||
.pathMatchers("/actuator/**") | ||
// all requests to `/actuator` should be sent only from inside the cluster | ||
// access to this port should be controlled by a NetworkPolicy | ||
.permitAll() | ||
.and() | ||
.authorizeExchange() | ||
.pathMatchers("/**") | ||
.authenticated() | ||
.and() | ||
.serviceAccountTokenAuthentication(serviceAccountTokenExtractorConverter, serviceAccountAuthenticatingManager) | ||
.csrf() | ||
.disable() | ||
.logout() | ||
.disable() | ||
.formLogin() | ||
.disable() | ||
.build() | ||
} | ||
|
||
/** | ||
* No-op security config when not running in Kubernetes. | ||
* FixMe: can be removed in favor of common `WebSecurityConfig` from authService? | ||
*/ | ||
@ConditionalOnCloudPlatform(CloudPlatform.NONE) | ||
@Bean | ||
@Order(2) | ||
@Suppress( | ||
"MISSING_KDOC_CLASS_ELEMENTS", | ||
"MISSING_KDOC_ON_FUNCTION", | ||
"KDOC_WITHOUT_PARAM_TAG", | ||
"KDOC_WITHOUT_RETURN_TAG", | ||
) | ||
fun internalInsecureSecurityChain( | ||
http: ServerHttpSecurity | ||
): SecurityWebFilterChain = http.run { | ||
securityMatcher( | ||
ServerWebExchangeMatchers.pathMatchers("/internal/**", "/actuator/**") | ||
) | ||
.authorizeExchange() | ||
.pathMatchers("/internal/**", "/actuator/**") | ||
.permitAll() | ||
.and() | ||
.csrf() | ||
.disable() | ||
.build() | ||
} | ||
} | ||
|
||
/** | ||
* A [ServerAuthenticationConverter] that attempts to convert a [ServerWebExchange] to an [Authentication] | ||
* if it encounters a SA token in [SA_HEADER_NAME] header. | ||
*/ | ||
@Component | ||
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) | ||
class ServiceAccountTokenExtractorConverter : ServerAuthenticationConverter { | ||
@Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") | ||
private val logger = getLogger<ServiceAccountTokenExtractorConverter>() | ||
override fun convert(exchange: ServerWebExchange): Mono<Authentication> = Mono.justOrEmpty( | ||
exchange.request.headers[SA_HEADER_NAME]?.firstOrNull() | ||
).map { token -> | ||
logger.debug { "Starting to process `$SA_HEADER_NAME` of an incoming request [${exchange.request.method} ${exchange.request.uri}]" } | ||
PreAuthenticatedAuthenticationToken("TokenSupplier", token) | ||
} | ||
} | ||
|
||
/** | ||
* A [ReactiveAuthenticationManager] that is intended to be used together with [ServerAuthenticationConverter]. | ||
* Attempts to authenticate an [Authentication] validating ServiceAccount token using TokeReview API. | ||
*/ | ||
@Component | ||
@ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) | ||
class ServiceAccountAuthenticatingManager( | ||
private val kubernetesClient: KubernetesClient, | ||
) : ReactiveAuthenticationManager { | ||
@Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") | ||
private val logger = getLogger<ServiceAccountAuthenticatingManager>() | ||
override fun authenticate(authentication: Authentication): Mono<Authentication> = authentication.toMono() | ||
.filter { it is PreAuthenticatedAuthenticationToken } | ||
.map { preAuthenticatedAuthenticationToken -> | ||
val tokenReview = tokenReviewSpec(preAuthenticatedAuthenticationToken.credentials as String) | ||
logger.debug { | ||
"Will create k8s resource from the following YAML:\n${tokenReview.prependIndent(" ")}" | ||
} | ||
val response = kubernetesClient.resource(tokenReview).createOrReplace() as TokenReview | ||
logger.debug { | ||
"Got the following response from the API server:\n${ | ||
Serialization.yamlMapper().writeValueAsString(response).prependIndent(" ") | ||
}" | ||
} | ||
response | ||
} | ||
.filter { response -> | ||
val isAuthenticated = response.status.error.isNullOrEmpty() && response.status.authenticated | ||
logger.debug { "After the response from TokenReview, request authentication is $isAuthenticated" } | ||
isAuthenticated | ||
} | ||
.map<Authentication> { | ||
with(authentication) { | ||
UsernamePasswordAuthenticationToken.authenticated(principal, credentials, authorities) | ||
} | ||
} | ||
.switchIfEmpty { | ||
Mono.error { BadCredentialsException("Invalid token") } | ||
} | ||
|
||
@Language("YAML") | ||
private fun tokenReviewSpec(token: String): String = """ | ||
|apiVersion: authentication.k8s.io/v1 | ||
|kind: TokenReview | ||
|metadata: | ||
| name: service-account-validity-check | ||
| namespace: ${kubernetesClient.namespace} | ||
|spec: | ||
| token: $token | ||
""".trimMargin() | ||
} | ||
|
||
/** | ||
* Configures authentication and authorization using Kubernetes ServiceAccount tokens. | ||
* This method requires two beans which can be imported with [KubernetesAuthenticationUtils] configuration class. | ||
*/ | ||
@Suppress("KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG") | ||
fun ServerHttpSecurity.serviceAccountTokenAuthentication( | ||
serviceAccountTokenExtractorConverter: ServiceAccountTokenExtractorConverter, | ||
serviceAccountAuthenticatingManager: ServiceAccountAuthenticatingManager, | ||
): ServerHttpSecurity = addFilterBefore( | ||
AuthenticationWebFilter(serviceAccountAuthenticatingManager).apply { | ||
setServerAuthenticationConverter(serviceAccountTokenExtractorConverter) | ||
}, | ||
SecurityWebFiltersOrder.HTTP_BASIC | ||
) | ||
.exceptionHandling { | ||
it.authenticationEntryPoint( | ||
HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED) | ||
) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check failure
Code scanning / ktlint
[KDOC_WITHOUT_RETURN_TAG] all methods which return values should have @return tag in KDoc: serviceAccountTokenHeaderWebClientCustomizer