From 79c6b9a80461662a701120d8a2a300f0d1469345 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Mon, 8 Jul 2024 08:43:20 +0200 Subject: [PATCH 1/2] chore: add components to commitlint.config.js --- commitlint.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commitlint.config.js b/commitlint.config.js index eb6dbd2..de42ffd 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -6,6 +6,10 @@ module.exports = { "always", [ "deps", + "instances-manager", + "workspace-client", + "workspace-job", + "workspace-manager" ], ], "subject-case": [0, 'never'], From 3f9bb4ee8dc3580959e6e3e263de36164143b6a0 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Fri, 5 Jul 2024 15:49:46 +0200 Subject: [PATCH 2/2] fix(workspace-manager): do not fall back on a hard-coded encryption key Only attempt to read secret from file. If it fails, do not start up the workspace manager. This should prevent accidental misconfiguration. --- .../manager/CredentialsEncryption.kt | 58 +++++++++++++++++++ .../workspace/manager/GitRepositoryManager.kt | 10 ++-- .../workspace/manager/WorkspaceManager.kt | 5 +- .../manager/WorkspaceManagerModule.kt | 27 +++++---- .../src/test/kotlin/CredentialsTest.kt | 11 ++-- .../org/modelix/workspaces/Workspace.kt | 58 ++----------------- .../workspaces/WorkspacePersistence.kt | 3 +- 7 files changed, 94 insertions(+), 78 deletions(-) create mode 100644 workspace-manager/src/main/kotlin/org/modelix/workspace/manager/CredentialsEncryption.kt diff --git a/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/CredentialsEncryption.kt b/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/CredentialsEncryption.kt new file mode 100644 index 0000000..a4a173e --- /dev/null +++ b/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/CredentialsEncryption.kt @@ -0,0 +1,58 @@ +package org.modelix.workspace.manager + +import org.jasypt.util.text.AES256TextEncryptor +import org.modelix.workspaces.Credentials +import org.modelix.workspaces.GitRepository +import org.modelix.workspaces.Workspace + +/** + * + */ +class CredentialsEncryption(key: String) { + companion object { + private const val ENCRYPTED_PREFIX = "encrypted:" + } + + + private val encryptor = AES256TextEncryptor() + + init { + encryptor.setPassword(key) + } + + fun decrypt(credentials: Credentials): Credentials { + return Credentials(decrypt(credentials.user), decrypt(credentials.password)) + } + + fun encrypt(credentials: Credentials): Credentials { + return Credentials(encrypt(credentials.user), encrypt(credentials.password)) + } + + private fun encrypt(input: String): String { + return if (input.startsWith(ENCRYPTED_PREFIX)) { + input + } else { + ENCRYPTED_PREFIX + encryptor.encrypt(input) + } + } + + private fun decrypt(input: String): String { + return if (input.startsWith(ENCRYPTED_PREFIX)) { + encryptor.decrypt(input.drop(ENCRYPTED_PREFIX.length)) + } else { + input + } + } +} + +fun CredentialsEncryption.copyWithEncryptedCredentials(workspace: Workspace): Workspace = + workspace.copy(gitRepositories = workspace.gitRepositories.map(::copyWithEncryptedCredentials)) + +fun CredentialsEncryption.copyWithDecryptedCredentials(workspace: Workspace): Workspace = + workspace.copy(gitRepositories = workspace.gitRepositories.map(::copyWithDecryptedCredentials)) + +fun CredentialsEncryption.copyWithEncryptedCredentials(gitRepository: GitRepository): GitRepository = + gitRepository.copy(credentials = gitRepository.credentials?.run(::encrypt)) + +fun CredentialsEncryption.copyWithDecryptedCredentials(gitRepository: GitRepository): GitRepository = + gitRepository.copy(credentials = gitRepository.credentials?.run(::decrypt)) \ No newline at end of file diff --git a/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/GitRepositoryManager.kt b/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/GitRepositoryManager.kt index 806b928..5924732 100644 --- a/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/GitRepositoryManager.kt +++ b/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/GitRepositoryManager.kt @@ -26,7 +26,6 @@ import org.eclipse.jgit.transport.http.JDKHttpConnection import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory import org.modelix.workspaces.GitRepository import java.io.File -import java.io.OutputStream import java.net.* import java.util.zip.ZipOutputStream @@ -64,14 +63,13 @@ class GitRepositoryManager(val config: GitRepository, val workspaceDirectory: Fi } private fun , T, E : TransportCommand> applyCredentials(cmd: E): E { - val encryptedCredentials = config.credentials - if (encryptedCredentials != null) { - val decrypted = encryptedCredentials.decrypt() - cmd.setCredentialsProvider(UsernamePasswordCredentialsProvider(decrypted.user, decrypted.password)) + val credentials = config.credentials + if (credentials != null) { + cmd.setCredentialsProvider(UsernamePasswordCredentialsProvider(credentials.user, credentials.password)) cmd.setTransportConfigCallback { transport -> transport?.setAuthenticator(object : Authenticator() { override fun getPasswordAuthentication(): PasswordAuthentication { - return PasswordAuthentication(decrypted.user, decrypted.password.toCharArray()) + return PasswordAuthentication(credentials.user, credentials.password.toCharArray()) } }) } diff --git a/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/WorkspaceManager.kt b/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/WorkspaceManager.kt index 960d044..ff864d3 100644 --- a/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/WorkspaceManager.kt +++ b/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/WorkspaceManager.kt @@ -20,7 +20,7 @@ import org.modelix.workspaces.WorkspaceHash import org.modelix.workspaces.WorkspacePersistence import java.io.File -class WorkspaceManager { +class WorkspaceManager(private val credentialsEncryption: CredentialsEncryption) { private val workspacePersistence = WorkspacePersistence() private val directory: File = run { // The workspace will contain git repositories. Avoid cloning them into an existing repository. @@ -38,7 +38,8 @@ class WorkspaceManager { @Synchronized fun update(workspace: Workspace): WorkspaceHash { - val hash = workspacePersistence.update(workspace) + val workspaceWithEncryptedCredentials = credentialsEncryption.copyWithEncryptedCredentials(workspace) + val hash = workspacePersistence.update(workspaceWithEncryptedCredentials) synchronized(buildJobs) { buildJobs.removeByWorkspaceId(workspace.id) FileUtils.deleteQuietly(getDownloadFile(hash)) diff --git a/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/WorkspaceManagerModule.kt b/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/WorkspaceManagerModule.kt index bf76e64..3ed5ab8 100644 --- a/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/WorkspaceManagerModule.kt +++ b/workspace-manager/src/main/kotlin/org/modelix/workspace/manager/WorkspaceManagerModule.kt @@ -43,7 +43,9 @@ import java.util.zip.ZipOutputStream fun Application.workspaceManagerModule() { - val manager = WorkspaceManager() + + val credentialsEncryption = createCredentialEncryption() + val manager = WorkspaceManager(credentialsEncryption) val maxBodySize = environment.config.property("modelix.maxBodySize").getString() install(Routing) @@ -259,7 +261,8 @@ fun Application.workspaceManagerModule() { return@intercept } val repo = repos[repoIndex] - val repoManager = GitRepositoryManager(repo, manager.getWorkspaceDirectory(workspace)) + val gitRepoWitDecryptedCredentials = credentialsEncryption.copyWithDecryptedCredentials(repo) + val repoManager = GitRepositoryManager(gitRepoWitDecryptedCredentials, manager.getWorkspaceDirectory(workspace)) if (!repoManager.repoDirectory.exists()) { repoManager.updateRepo() } @@ -728,13 +731,7 @@ fun Application.workspaceManagerModule() { val decryptCredentials = call.request.queryParameters["decryptCredentials"] == "true" val decrypted = if (decryptCredentials) { // TODO check permission to read decrypted credentials - workspace.copy( - gitRepositories = workspace.gitRepositories.map { - it.copy( - credentials = it.credentials?.decrypt() - ) - } - ) + credentialsEncryption.copyWithDecryptedCredentials(workspace) } else { workspace } @@ -755,7 +752,8 @@ fun Application.workspaceManagerModule() { return@get } - val gitRepoManager = GitRepositoryManager(gitRepo, manager.getWorkspaceDirectory(workspace)) + val gitRepoWitDecryptedCredentials = credentialsEncryption.copyWithDecryptedCredentials(gitRepo) + val gitRepoManager = GitRepositoryManager(gitRepoWitDecryptedCredentials, manager.getWorkspaceDirectory(workspace)) gitRepoManager.updateRepo() call.respondOutputStream(ContentType.Application.Zip) { ZipOutputStream(this).use { zip -> @@ -875,6 +873,15 @@ fun Application.workspaceManagerModule() { } } +private fun createCredentialEncryption(): CredentialsEncryption { + // Secrets mounted as files are more secure than environment variables + // because environment variables can more easily leak or be extracted. + // See https://stackoverflow.com/questions/51365355/kubernetes-secrets-volumes-vs-environment-variables + val credentialsEncryptionKeyFile = File("/secrets/workspacesecret/workspace-credentials-key.txt") + val credentialsEncryptionKey = credentialsEncryptionKeyFile.readLines().first() + return CredentialsEncryption(credentialsEncryptionKey) +} + private fun findGitRepo(folder: File): File? { if (!folder.exists()) return null if (folder.name == ".git") return folder.parentFile diff --git a/workspace-manager/src/test/kotlin/CredentialsTest.kt b/workspace-manager/src/test/kotlin/CredentialsTest.kt index 024c357..7a64334 100644 --- a/workspace-manager/src/test/kotlin/CredentialsTest.kt +++ b/workspace-manager/src/test/kotlin/CredentialsTest.kt @@ -3,15 +3,18 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import org.modelix.workspace.manager.CredentialsEncryption import org.modelix.workspaces.Credentials class CredentialsTest { + private val credentialsEncryption = CredentialsEncryption("aKey") + @Test fun testEncryption() { val credentials = Credentials("user", "1A.23bc\$") - val encrypted = credentials.encrypt() - val decrypted = encrypted.decrypt() + val encrypted = credentialsEncryption.encrypt(credentials) + val decrypted = credentialsEncryption.decrypt(encrypted) Assertions.assertEquals(credentials.password, decrypted.password) Assertions.assertArrayEquals(credentials.password.toByteArray(), decrypted.password.toByteArray()) } @@ -21,10 +24,10 @@ class CredentialsTest { val credentials = Credentials("user", "1A.23bc\$") val serialized = Yaml.default.encodeToString(credentials) val deserialized = Yaml.default.decodeFromString(serialized) - val encrypted = deserialized.encrypt() + val encrypted = credentialsEncryption.encrypt(deserialized) val serialized2 = Yaml.default.encodeToString(encrypted) val deserialized2 = Yaml.default.decodeFromString(serialized2) - val decrypted = deserialized2.decrypt() + val decrypted = credentialsEncryption.decrypt(deserialized2) Assertions.assertEquals(credentials.password, decrypted.password) Assertions.assertArrayEquals(credentials.password.toByteArray(), decrypted.password.toByteArray()) } diff --git a/workspaces/src/main/kotlin/org/modelix/workspaces/Workspace.kt b/workspaces/src/main/kotlin/org/modelix/workspaces/Workspace.kt index fc8878b..17061ba 100644 --- a/workspaces/src/main/kotlin/org/modelix/workspaces/Workspace.kt +++ b/workspaces/src/main/kotlin/org/modelix/workspaces/Workspace.kt @@ -99,65 +99,15 @@ data class Binding(val project: String? = null, data class GitRepository(val url: String, val name: String? = null, val branch: String = "master", - var commitHash: String? = null, + val commitHash: String? = null, val paths: List = listOf(), - var credentials: Credentials? = null) + val credentials: Credentials? = null) @Serializable -data class Credentials(val user: String, val password: String) { - fun decrypt(): Credentials { - return Credentials(decrypt(user), decrypt(password)) - } - - fun encrypt(): Credentials { - return Credentials(encrypt(user), encrypt(password)) - } - - private fun encrypt(input: String): String { - return if (input.startsWith(ENCRYPTED_PREFIX)) { - input - } else { - ENCRYPTED_PREFIX + credentialsEncryptor.encrypt(input) - } - } - - private fun decrypt(input: String): String { - return if (input.startsWith(ENCRYPTED_PREFIX)) { - credentialsEncryptor.decrypt(input.drop(ENCRYPTED_PREFIX.length)) - } else { - input - } - } -} +data class Credentials(val user: String, val password: String) @Serializable data class MavenRepository(val url: String) @Serializable -data class SharedInstance(val name: String = "shared", val allowWrite: Boolean = false) - -private const val ENCRYPTED_PREFIX = "encrypted:" -private val credentialsEncryptor = run { - val encryptor = AES256TextEncryptor() - val key = try { - val kubernetesSecretFile = File("/secrets/workspacesecret/workspace-credentials-key.txt") - if (kubernetesSecretFile.exists()) { - kubernetesSecretFile.readLines().first() - } else { - println("${kubernetesSecretFile.absolutePath} doesn't exist") - val localSecretFile = File("../kubernetes/secrets/workspace-credentials-key.txt") - if (localSecretFile.exists()) { - localSecretFile.readLines().first() - } else { - val rand = SecureRandom() - val k = (1..200).map { rand.nextInt(10) }.joinToString("") { it.toString() } - localSecretFile.writeText(k, StandardCharsets.UTF_8) - k - } - } - } catch (e: Exception) { - "g5Mu5trXweUQmijZtZrmjCel81O64FyVxbNFFXgOfsaUYdJ5oZ89BIN8TQy20nQskDOHK0fzJ3bnkqZLUeO6Jng2WR2WJY1V8ef86pyWj6HO5c" - } - encryptor.setPassword(key) - encryptor -} \ No newline at end of file +data class SharedInstance(val name: String = "shared", val allowWrite: Boolean = false) \ No newline at end of file diff --git a/workspaces/src/main/kotlin/org/modelix/workspaces/WorkspacePersistence.kt b/workspaces/src/main/kotlin/org/modelix/workspaces/WorkspacePersistence.kt index cbfb922..c8c0fd8 100644 --- a/workspaces/src/main/kotlin/org/modelix/workspaces/WorkspacePersistence.kt +++ b/workspaces/src/main/kotlin/org/modelix/workspaces/WorkspacePersistence.kt @@ -28,7 +28,7 @@ val workspaceListResource = KeycloakResourceType("list", setOf(KeycloakScope.ADD .createInstance("workspace-list") val workspaceUploadResourceType = KeycloakResourceType("workspace-upload", setOf(KeycloakScope.READ, KeycloakScope.DELETE), createByUser = true) -class WorkspacePersistence() { +class WorkspacePersistence { private val WORKSPACE_LIST_KEY = "workspaces" private val modelClient: RestWebModelClient = RestWebModelClient(getModelServerUrl(), authTokenProvider = serviceAccountTokenProvider) @@ -105,7 +105,6 @@ class WorkspacePersistence() { require(mpsVersion == null || mpsVersion.matches(Regex("""20\d\d\.\d"""))) { "Invalid major MPS version: '$mpsVersion'. Examples for valid values: '2020.3', '2021.1', '2021.2'." } - workspace.gitRepositories.forEach { it.credentials = it.credentials?.encrypt() } val id = workspace.id val json = Json.encodeToString(workspace) val hash = WorkspaceHash(HashUtil.sha256(json))