add port to KnownHost

This commit is contained in:
ansgarz 2023-08-27 12:54:21 +02:00
parent 111d9951ed
commit 2fff923539
3 changed files with 77 additions and 20 deletions

View file

@ -1,26 +1,35 @@
package org.domaindrivenarchitecture.provs.desktop.domain package org.domaindrivenarchitecture.provs.desktop.domain
typealias HostKey = String
/** /**
* A HostKey should contain space-separated: keytype, key and (optionally) a comment * Represents a known host for ssh connections.
*
* @param hostName domain name or ip
* @param port (optional) to be specified if different from default port 22
* @param hostKeys list of keys, where each should contain separated by space: 1. keytype, 2. key and 3. (optionally) a comment
* *
* See: https://man7.org/linux/man-pages/man8/sshd.8.html#SSH_KNOWN_HOSTS_FILE_FORMAT * See: https://man7.org/linux/man-pages/man8/sshd.8.html#SSH_KNOWN_HOSTS_FILE_FORMAT
*/ */
typealias HostKey = String
open class KnownHost( open class KnownHost(
val hostName: String, val hostName: String,
val port: Int? = null,
val hostKeys: List<HostKey> val hostKeys: List<HostKey>
) { ) {
constructor(hostName: String, hostKeys: List<HostKey>) : this(hostName, null, hostKeys)
companion object { companion object {
val GITHUB = KnownHost( val GITHUB = KnownHost(
"github.com", listOf( "github.com",
listOf(
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl",
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=",
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=",
) )
) )
val GITLAB = KnownHost( val GITLAB = KnownHost(
"gitlab.com", listOf( "gitlab.com",
listOf(
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf",
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9",
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=", "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=",

View file

@ -19,18 +19,21 @@ fun Prov.configureSshKeys(sshKeys: SshKeyPair) = task {
/** /**
* Checks if the specified hostname or Ip is in a known_hosts file * Checks if the specified host (domain name or IP) and (optional) port is contained in the known_hosts file
*
* @return whether if was found
*/ */
fun Prov.isKnownHost(hostOrIp: String): Boolean { fun Prov.isKnownHost(hostOrIp: String, port: Int? = null): Boolean {
return cmdNoEval("ssh-keygen -F $hostOrIp").out?.isNotEmpty() ?: false val hostWithPotentialPort = port?.let { hostInKnownHostsFileFormat(hostOrIp, port) } ?: hostOrIp
return cmdNoEval("ssh-keygen -F $hostWithPotentialPort").out?.isNotEmpty() ?: false
}
fun hostInKnownHostsFileFormat(hostOrIp: String, port: Int? = null): String {
return port?.let { "[$hostOrIp]:$port" } ?: hostOrIp
} }
/** /**
* Adds ssh keys for specified host (which also can be an ip-address) to the ssh-file "known_hosts". * Adds ssh keys for specified host (which also can be an ip-address) to the ssh-file "known_hosts".
* If parameter verifyKeys is true the keys are checked against the live keys of the host and only added if valid. * If parameter verifyKeys is true, the keys are checked against the live keys of the host and added only if valid.
*/ */
fun Prov.addKnownHost(knownHost: KnownHost, verifyKeys: Boolean = false) = task { fun Prov.addKnownHost(knownHost: KnownHost, verifyKeys: Boolean = false) = task {
val knownHostsFile = "~/.ssh/known_hosts" val knownHostsFile = "~/.ssh/known_hosts"
@ -44,9 +47,10 @@ fun Prov.addKnownHost(knownHost: KnownHost, verifyKeys: Boolean = false) = task
if (!verifyKeys) { if (!verifyKeys) {
addTextToFile("\n$hostName $key\n", File(knownHostsFile)) addTextToFile("\n$hostName $key\n", File(knownHostsFile))
} else { } else {
val validKeys = getSshKeys(hostName) val validKeys = findSshKeys(hostName, port)
if (validKeys?.contains(key) == true) { if (validKeys?.contains(key) == true) {
addTextToFile("\n$hostName $key\n", File(knownHostsFile)) val formattedHost = hostInKnownHostsFileFormat(hostName, port)
addTextToFile("\n$formattedHost $key\n", File(knownHostsFile))
} else { } else {
addResultToEval( addResultToEval(
ProvResult( ProvResult(
@ -62,10 +66,14 @@ fun Prov.addKnownHost(knownHost: KnownHost, verifyKeys: Boolean = false) = task
/** /**
* Returns a list of valid ssh keys for the given host (host can also be an ip address), keys are returned as keytype and key BUT WITHOUT the host name * Returns a list of valid ssh keys for the given host (host can also be an ip address),
* keys are returned (space-separated) as keytype and key, but WITHOUT the host name.*
* If no port is specified, the keys for the default port (22) are returned.
* If no keytype is specified, keys are returned for all keytypes.
*/ */
private fun Prov.getSshKeys(host: String, keytype: String? = null): List<String>? { fun Prov.findSshKeys(host: String, port: Int? = null, keytype: String? = null): List<String>? {
val portOption = port?.let { " -p $port " } ?: ""
val keytypeOption = keytype?.let { " -t $keytype " } ?: "" val keytypeOption = keytype?.let { " -t $keytype " } ?: ""
val output = cmd("ssh-keyscan $keytypeOption $host 2>/dev/null").out?.trim() val output = cmd("ssh-keyscan $portOption $keytypeOption $host 2>/dev/null").out?.trim()
return output?.split("\n")?.filter { x -> "" != x }?.map { x -> x.substringAfter(" ") } return output?.split("\n")?.filter { x -> "" != x }?.map { x -> x.substringAfter(" ") }
} }

View file

@ -2,10 +2,11 @@ package org.domaindrivenarchitecture.provs.desktop.domain
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.deleteFile import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.deleteFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.addKnownHost
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.isKnownHost
import org.domaindrivenarchitecture.provs.test.defaultTestContainer import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -29,10 +30,10 @@ class KnownHostTest {
// Subclass of KnownHost for test knownHostSubclass_includes_additional_host // Subclass of KnownHost for test knownHostSubclass_includes_additional_host
class KnownHostsSubclass(hostName: String, hostKeys: List<HostKey>): KnownHost(hostName, hostKeys) { class KnownHostsSubclass(hostName: String, port: Int?, hostKeys: List<HostKey>): KnownHost(hostName, port, hostKeys) {
companion object { companion object {
val ANOTHER_HOST = KnownHostsSubclass("anotherhost.com", listOf("key1")) val ANOTHER_HOST = KnownHostsSubclass("anotherhost.com", 2222, listOf("key1"))
fun values(): List<KnownHost> { fun values(): List<KnownHost> {
return values + ANOTHER_HOST return values + ANOTHER_HOST
@ -49,5 +50,44 @@ class KnownHostTest {
assertTrue(hosts.size > 1) assertTrue(hosts.size > 1)
assertEquals("key1", hosts.last().hostKeys[0]) assertEquals("key1", hosts.last().hostKeys[0])
} }
@ContainerTest
fun knownHost_with_port_verified_successfully() {
// given
val prov = defaultTestContainer()
prov.task {
aptInstall("ssh")
deleteFile("~/.ssh/known_hosts")
}
// when
assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName))
assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName, 22))
val res = prov.addKnownHost(KnownHost(KnownHost.GITHUB.hostName, 22, KnownHost.GITHUB.hostKeys), verifyKeys = true)
// then
assertTrue(res.success)
assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName))
assertTrue(prov.isKnownHost(KnownHost.GITHUB.hostName, 22))
}
@ContainerTest
fun knownHost_with_port_verification_failing() {
// given
val prov = defaultTestContainer()
prov.task {
aptInstall("ssh")
deleteFile("~/.ssh/known_hosts")
}
// when
assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName, 80))
val res2 = prov.addKnownHost(KnownHost(KnownHost.GITHUB.hostName, 80, KnownHost.GITHUB.hostKeys), verifyKeys = true)
// then
assertFalse(res2.success)
assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName))
assertFalse(prov.isKnownHost(KnownHost.GITHUB.hostName, 80))
}
} }