Skip to content
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 agent #1247

Draft
wants to merge 8 commits into
base: feature/authenticated-microservices
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import kotlinx.serialization.Serializable
* @property debug whether debug logging should be enabled
* @property testSuitesDir directory where tests and additional files need to be stored into
* @property logFilePath path to logs of save-cli execution
* @property parentUserName name of a parent process user, needed for token isolation
* @property childUserName name of a child process user, needed for token isolation
* @property save additional configuration for save-cli
* @property kubernetes a flag which shows that agent runs in k8s
*/
Expand All @@ -39,6 +41,8 @@ data class AgentConfiguration(
val debug: Boolean = false,
val testSuitesDir: String = TEST_SUITES_DIR_NAME,
val logFilePath: String = "logs.txt",
val parentUserName: String? = null,
val childUserName: String? = null,
val kubernetes: Boolean = false,
val save: SaveCliConfig = SaveCliConfig(),
) {
Expand All @@ -55,6 +59,8 @@ data class AgentConfiguration(
heartbeat = HeartbeatConfig(
url = requiredEnv(AgentEnvName.HEARTBEAT_URL.name),
),
parentUserName = optionalEnv(AgentEnvName.PARENT_PROCESS_USERNAME.name),
childUserName = optionalEnv(AgentEnvName.CHILD_PROCESS_USERNAME.name),
kubernetes = optionalEnv(AgentEnvName.KUBERNETES.name).toBoolean(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import com.saveourtool.save.agent.utils.ktorLogger
import com.saveourtool.save.core.config.LogType
import com.saveourtool.save.core.logging.describe
import com.saveourtool.save.core.logging.logType
import com.saveourtool.save.utils.KubernetesServiceAccountAuthHeaderPlugin
import com.saveourtool.save.utils.fs
import com.saveourtool.save.utils.parseConfig
import com.saveourtool.save.utils.*

import io.ktor.client.HttpClient
import io.ktor.client.plugins.*
Expand Down Expand Up @@ -51,9 +49,10 @@ fun main() {
.updateFromEnv()
logType.set(if (config.debug) LogType.ALL else LogType.WARN)
logDebugCustom("Instantiating save-agent version ${config.info.version} with config $config")

handleSigterm()

config.parentUserName?.let { protectAuthToken(it, it) }

val httpClient = configureHttpClient(config)

runBlocking {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class SaveAgent(
?.let { fileName ->
val targetFile = targetDirectory / fileName
logDebugCustom("Additionally setup of evaluated tool by $targetFile")
// todo: protect me after ProcessBuilder is updated (https://github.com/saveourtool/save-cli/issues/521)
val setupResult = ProcessBuilder(true, fs)
.exec(
"./$targetFile",
Expand Down Expand Up @@ -287,6 +288,9 @@ class SaveAgent(

private fun runSave(cliArgs: String): ExecutionResult {
val fullCliCommand = buildString {
config.childUserName?.let { userName ->
append("sudo -u $userName ")
}
append(config.cliCommand)
append(" ${config.testSuitesDir}")
append(" $cliArgs")
Expand Down
2 changes: 2 additions & 0 deletions save-cloud-charts/save-cloud/templates/demo-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ data:

demo.backend-url=http://backend/internal
demo.agent-config.demo-url=http://demo
demo.agent-config.parent-user-name={{ .Values.agentParentUserName }}
demo.agent-config.child-user-name={{ .Values.agentChildUserName }}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ data:
management.server.port={{ .Values.orchestrator.managementPort }}
orchestrator.agent-settings.heartbeat-url=http://{{ .Values.orchestrator.name }}/heartbeat
orchestrator.agent-settings.debug=true
orchestrator.agent-settings.parent-user-name={{ .Values.agentParentUserName }}
orchestrator.agent-settings.child-user-name={{ .Values.agentChildUserName }}

logging.level.com.saveourtool.save.orchestrator.kubernetes=DEBUG
logging.level.org.springframework=DEBUG
Expand Down
2 changes: 2 additions & 0 deletions save-cloud-charts/save-cloud/templates/sandbox-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ data:
sandbox.agent-settings.sandbox-url=http://{{ .Values.sandbox.name }}
orchestrator.agent-settings.heartbeat-url=http://{{ .Values.sandbox.name }}/heartbeat
orchestrator.agent-settings.debug=true
orchestrator.agent-settings.parent-user-name={{ .Values.agentParentUserName }}
orchestrator.agent-settings.child-user-name={{ .Values.agentChildUserName }}

logging.level.com.saveourtool.save.orchestrator.kubernetes=DEBUG

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
namespace: save-agent-network-policy
spec:
podSelector:
matchLabels:
io.kompose.service: save-agent
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
io.kompose.service: orchestrator
- to:
# https://stackoverflow.com/q/73049535
- ipBlock:
cidr: 0.0.0.0/0
# Forbid private IP ranges effectively allowing only egress to the Internet
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: "kube-system"
- podSelector:
matchLabels:
k8s-app: "kube-dns"
2 changes: 2 additions & 0 deletions save-cloud-charts/save-cloud/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,5 @@ demo:

namespace: save-cloud
agentNamespace: save-agent
agentParentUserName: save-agent
agentChildUserName: save-executor
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ package com.saveourtool.save.agent
* Env names which agent supports and expects
*/
enum class AgentEnvName {
CHILD_PROCESS_USERNAME,
CLI_COMMAND,
CONTAINER_ID,
CONTAINER_NAME,
DEBUG,
EXECUTION_ID,
HEARTBEAT_URL,
PARENT_PROCESS_USERNAME,
KUBERNETES,
TEST_SUITES_DIR,
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ import kotlinx.serialization.Serializable
* @property demoConfiguration all the information about current demo e.g. maintainer and version
* @property runConfiguration all the required information to run demo
* @property demoUrl url of save-demo
* @property parentUserName name of a parent process user, needed for token isolation
* @property childUserName name of a child process user, needed for token isolation
* @property setupShTimeoutMillis amount of milliseconds to run setup.sh if it is present, [DEFAULT_SETUP_SH_TIMEOUT_MILLIS] by default
*/
@Serializable
data class DemoAgentConfig(
val demoUrl: String,
val demoConfiguration: DemoConfiguration,
val runConfiguration: RunConfiguration,
val parentUserName: String?,
val childUserName: String?,
val setupShTimeoutMillis: Long = DEFAULT_SETUP_SH_TIMEOUT_MILLIS,
) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,5 @@ const val AUTHORIZATION_SOURCE = "X-Authorization-Source"
*/
@Suppress("NON_EXPORTABLE_TYPE")
const val DEFAULT_SETUP_SH_TIMEOUT_MILLIS: Long = 60_000L

const val DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ import okio.Path.Companion.toPath
expect val fs: FileSystem

/**
* Mark [this] file as executable. Sets permissions to rwxr--r--
* Sets permissions to [this] file to r--|---|---
*
* @param ownerName name of owner user
* @param groupName name of group
*/
expect fun Path.permitReadingOnlyForOwner(ownerName: String, groupName: String)

/**
* Mark [this] file as executable. Sets permissions to rwx|r--|r--
*/
expect fun Path.markAsExecutable()

Expand Down Expand Up @@ -98,3 +106,17 @@ inline fun <reified C : Any> parseConfigOrDefault(
logInfo("Config file $configName not found, falling back to default config.")
defaultConfig
}

/**
* Allow reading file with path [tokenPathString] to owner only
*
* @param ownerName name of an owner to permit reading
* @param groupName name of an owner's group to permit reading
* @param tokenPathString path to token file
* @return Unit
*/
fun protectAuthToken(
ownerName: String,
groupName: String,
tokenPathString: String = DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH
) = tokenPathString.toPath().permitReadingOnlyForOwner(ownerName, groupName)
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ val KubernetesServiceAccountAuthHeaderPlugin = createClientPlugin(
val token = ExpiringValueWrapper(pluginConfig.expirationTime) {
fs.read(pluginConfig.tokenPath.toPath()) { readUtf8() }
}
val headerName = pluginConfig.headerName
onRequest { request, _ ->
request.headers.append(SA_HEADER_NAME, token.getValue())
request.headers.append(headerName, token.getValue())
}
}

Expand All @@ -47,7 +48,7 @@ class KubernetesServiceAccountAuthHeaderPluginConfig {
/**
* Kubernetes service account token path configuration
*/
var tokenPath: String = "/var/run/secrets/kubernetes.io/serviceaccount/token"
var tokenPath: String = DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH

/**
* Token expiration [Duration] configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const val NOT_IMPLEMENTED_ON_JS = "Cannot be used in js."

@Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION")
actual val fs: FileSystem by lazy { throw NotImplementedError(NOT_IMPLEMENTED_ON_JS) }

actual fun Path.permitReadingOnlyForOwner(ownerName: String, groupName: String) {
throw NotImplementedError(NOT_IMPLEMENTED_ON_JS)
}

actual fun Path.markAsExecutable() {
throw NotImplementedError(NOT_IMPLEMENTED_ON_JS)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.*
import java.util.*
import java.util.stream.Collectors

Expand Down Expand Up @@ -129,6 +129,20 @@ fun Path.requireIsAbsolute(): Path = apply {
}
}

actual fun okio.Path.permitReadingOnlyForOwner(ownerName: String, groupName: String) {
val file = toFile().toPath()
val fileAttributeView = Files.getFileAttributeView(file, PosixFileAttributeView::class.java)
val lookupService = file.fileSystem.userPrincipalLookupService

val owner = lookupService.lookupPrincipalByName(ownerName)
val group = lookupService.lookupPrincipalByGroupName(groupName)

fileAttributeView.owner = owner
fileAttributeView.setGroup(group)

Files.setPosixFilePermissions(file, EnumSet.of(PosixFilePermission.OWNER_READ))
}

actual fun okio.Path.markAsExecutable() {
val file = this.toFile().toPath()
Files.setPosixFilePermissions(file, Files.getPosixFilePermissions(file) + EnumSet.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,27 @@ import platform.posix.*
import kotlin.system.getTimeNanos
import kotlinx.cinterop.UnsafeNumber
import kotlinx.cinterop.convert
import kotlinx.cinterop.pointed
import kotlinx.serialization.serializer

actual val fs: FileSystem = FileSystem.SYSTEM

@OptIn(UnsafeNumber::class)
@Suppress("TooGenericExceptionThrown")
actual fun Path.permitReadingOnlyForOwner(ownerName: String, groupName: String) {
val owner = requireNotNull(getpwnam(ownerName)) { "Could not find user with name $ownerName" }
val group = requireNotNull(getgrnam(groupName)) { "Could not find group with name $groupName" }

if (chown(toString(), owner.pointed.pw_uid, group.pointed.gr_gid) != 0) {
throw RuntimeException("Could not change file owner or group")
}

val mode: mode_t = S_IRUSR.convert()
if (chmod(toString(), mode) != 0) {
throw RuntimeException("Could not change file permissions")
}
}

@OptIn(UnsafeNumber::class)
actual fun Path.markAsExecutable() {
val mode: mode_t = (S_IRUSR or S_IWUSR or S_IXUSR or S_IRGRP or S_IROTH).convert()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.saveourtool.save.demo.DemoRunRequest
import com.saveourtool.save.demo.ServerConfiguration
import com.saveourtool.save.demo.agent.utils.getConfiguration
import com.saveourtool.save.demo.agent.utils.setupEnvironment
import com.saveourtool.save.utils.protectAuthToken
import com.saveourtool.save.utils.retry

import io.ktor.http.*
Expand Down Expand Up @@ -52,10 +53,11 @@ private fun Application.getConfigurationOnStartup(
logDebug("Configuration successfully fetched.")
config
}
?.also { config -> config.parentUserName?.let { userName -> protectAuthToken(userName, userName) } }
?.also(updateConfig)
?.let { config ->
logTrace("Configuration successfully updated.")
setupEnvironment(config.demoUrl, config.setupShTimeoutMillis, config.demoConfiguration)
setupEnvironment(config.demoUrl, config.setupShTimeoutMillis, config.childUserName, config.demoConfiguration)
}
?: run { logWarn("Could not prepare save-demo-agent, expecting /setup call.") }
}
Expand All @@ -69,20 +71,31 @@ private fun Routing.alive(configuration: CompletableDeferred<DemoAgentConfig>) =
})
}

private fun Routing.configure(updateConfig: (DemoAgentConfig) -> Unit) = post("/setup") {
val config = call.receive<DemoAgentConfig>().also(updateConfig)
logInfo("Agent has received configuration.")
try {
setupEnvironment(config.demoUrl, config.setupShTimeoutMillis, config.demoConfiguration)
call.respondText(
"Agent is set up.",
status = HttpStatusCode.OK,
)
} catch (exception: IllegalStateException) {
private fun Routing.configure(
deferredConfig: CompletableDeferred<DemoAgentConfig>,
updateConfig: (DemoAgentConfig) -> Unit,
) = post("/setup") {
if (deferredConfig.isCompleted) {
call.respondText(
exception.message ?: "Internal agent error.",
status = HttpStatusCode.InternalServerError,
"save-demo-agent is already configured.",
status = HttpStatusCode.Conflict,
)
} else {
val config = call.receive<DemoAgentConfig>().also(updateConfig)
logInfo("Agent has received configuration.")
config.parentUserName?.let { parentUserName -> protectAuthToken(parentUserName, parentUserName) }
try {
setupEnvironment(config.demoUrl, config.setupShTimeoutMillis, config.childUserName, config.demoConfiguration)
call.respondText(
"Agent is set up.",
status = HttpStatusCode.OK,
)
} catch (exception: IllegalStateException) {
call.respondText(
exception.message ?: "Internal agent error.",
status = HttpStatusCode.InternalServerError,
)
}
}
}

Expand Down Expand Up @@ -121,7 +134,7 @@ fun server(serverConfiguration: ServerConfiguration, skipStartupConfiguration: B
install(ContentNegotiation) { json() }
routing {
alive(deferredConfig)
configure { deferredConfig.complete(it) }
configure(deferredConfig) { deferredConfig.complete(it) }
run(deferredConfig)
}
}
Loading