diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ssh.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ssh.json index 48513dab7dff8..dd1c9ac5740bf 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ssh.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ssh.json @@ -47,11 +47,11 @@ "certResource": { "index": 19, "kind": "property", "displayName": "Cert Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path of the certificate to use for Authentication. Will use ResourceHelperKeyPairProvider to resolve file based certificate, and depends on keyType setting." }, "certResourcePassword": { "index": 20, "kind": "property", "displayName": "Cert Resource Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the password to use in loading certResource, if certResource is an encrypted key." }, "ciphers": { "index": 21, "kind": "property", "displayName": "Ciphers", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported ciphers in their order of preference." }, - "failOnUnknownHost": { "index": 22, "kind": "property", "displayName": "Fail On Unknown Host", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "false", "defaultValue": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Specifies whether a connection to an unknown SSH server host key should fail. When false, host keys that are not present in the known_hosts resource are accepted." }, + "failOnUnknownHost": { "index": 22, "kind": "property", "displayName": "Fail On Unknown Host", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "false", "defaultValue": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Specifies whether a connection to a server whose host has no entry without a marker in the known_hosts resource should fail. When false, such a server is accepted. A server whose host has entries without a marker is always rejected when it presents a key that none of them lists, and a key listed in a matching revoked entry is always rejected." }, "kex": { "index": 23, "kind": "property", "displayName": "Kex", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported key exchange algorithms in their order of preference." }, "keyPairProvider": { "index": 24, "kind": "property", "displayName": "Key Pair Provider", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.sshd.common.keyprovider.KeyPairProvider", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the KeyPairProvider reference to use when connecting using Certificates to the remote SSH Server." }, "keyType": { "index": 25, "kind": "property", "displayName": "Key Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the key type to pass to the KeyPairProvider as part of authentication. KeyPairProvider.loadKey(...) will be passed this value. From Camel 3.0.0 \/ 2.25.0, by default Camel will select the first available KeyPair that is loaded. Prior to this, a KeyType of 'ssh-rsa' was enforced by default." }, - "knownHostsResource": { "index": 26, "kind": "property", "displayName": "Known Hosts Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path for a known_hosts file used to verify the SSH server host key. When not set, the client does not verify the server host key against a known_hosts file." }, + "knownHostsResource": { "index": 26, "kind": "property", "displayName": "Known Hosts Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path for a known_hosts file used to verify the SSH server host key. The file uses the OpenSSH known_hosts format, and its entries are matched against the host and port configured on the endpoint. A resource that cannot be read, or that has a line that cannot be read, makes every connection fail. When not set, the client does not verify the server host key against a known_hosts file." }, "macs": { "index": 27, "kind": "property", "displayName": "Macs", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported message authentication code algorithms in their order of preference. The MAC algorithm is used for data integrity protection." }, "password": { "index": 28, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the password to use in connecting to remote SSH server. Requires keyPairProvider to be set to null." }, "signatures": { "index": 29, "kind": "property", "displayName": "Signatures", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported signature algorithms in their order of preference." }, @@ -102,11 +102,11 @@ "certResource": { "index": 35, "kind": "parameter", "displayName": "Cert Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path of the certificate to use for Authentication. Will use ResourceHelperKeyPairProvider to resolve file based certificate, and depends on keyType setting." }, "certResourcePassword": { "index": 36, "kind": "parameter", "displayName": "Cert Resource Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the password to use in loading certResource, if certResource is an encrypted key." }, "ciphers": { "index": 37, "kind": "parameter", "displayName": "Ciphers", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported ciphers in their order of preference." }, - "failOnUnknownHost": { "index": 38, "kind": "parameter", "displayName": "Fail On Unknown Host", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "false", "defaultValue": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Specifies whether a connection to an unknown SSH server host key should fail. When false, host keys that are not present in the known_hosts resource are accepted." }, + "failOnUnknownHost": { "index": 38, "kind": "parameter", "displayName": "Fail On Unknown Host", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "false", "defaultValue": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Specifies whether a connection to a server whose host has no entry without a marker in the known_hosts resource should fail. When false, such a server is accepted. A server whose host has entries without a marker is always rejected when it presents a key that none of them lists, and a key listed in a matching revoked entry is always rejected." }, "kex": { "index": 39, "kind": "parameter", "displayName": "Kex", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported key exchange algorithms in their order of preference." }, "keyPairProvider": { "index": 40, "kind": "parameter", "displayName": "Key Pair Provider", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.sshd.common.keyprovider.KeyPairProvider", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the KeyPairProvider reference to use when connecting using Certificates to the remote SSH Server." }, "keyType": { "index": 41, "kind": "parameter", "displayName": "Key Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the key type to pass to the KeyPairProvider as part of authentication. KeyPairProvider.loadKey(...) will be passed this value. From Camel 3.0.0 \/ 2.25.0, by default Camel will select the first available KeyPair that is loaded. Prior to this, a KeyType of 'ssh-rsa' was enforced by default." }, - "knownHostsResource": { "index": 42, "kind": "parameter", "displayName": "Known Hosts Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path for a known_hosts file used to verify the SSH server host key. When not set, the client does not verify the server host key against a known_hosts file." }, + "knownHostsResource": { "index": 42, "kind": "parameter", "displayName": "Known Hosts Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path for a known_hosts file used to verify the SSH server host key. The file uses the OpenSSH known_hosts format, and its entries are matched against the host and port configured on the endpoint. A resource that cannot be read, or that has a line that cannot be read, makes every connection fail. When not set, the client does not verify the server host key against a known_hosts file." }, "macs": { "index": 43, "kind": "parameter", "displayName": "Macs", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported message authentication code algorithms in their order of preference. The MAC algorithm is used for data integrity protection." }, "password": { "index": 44, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the password to use in connecting to remote SSH server. Requires keyPairProvider to be set to null." }, "signatures": { "index": 45, "kind": "parameter", "displayName": "Signatures", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported signature algorithms in their order of preference." }, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ssh-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ssh-component.adoc index 698ec5f305daa..e1b2e38de453a 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ssh-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ssh-component.adoc @@ -163,6 +163,72 @@ YAML:: An example of using Public Key authentication is provided in `examples/camel-example-ssh-security`. +=== Server host key verification + +The SSH Component verifies the host key of the SSH server only when the `knownHostsResource` option +is set. It refers to a file in the OpenSSH `known_hosts` format, such as the output of `ssh-keyscan`. +Set `failOnUnknownHost` to `true` as well, so that a server without any entry in that file is rejected. +When it is `false` (the default), such a server is accepted and a warning is logged. + +The entries are matched as OpenSSH matches them: + +* Each host pattern of an entry is compared, ignoring case, with the host name or address the client +connects to: the host configured on the endpoint, or the `HostName` that `~/.ssh/config` gives for it. +It is written `host` for port 22 and `[host]:port` for another port. `*` and `?` are wildcards, a +pattern starting with `!` excludes the hosts it matches, and hashed entries are supported. So +`db.example.com` only applies to port 22, `[db.example.com]:2222` to port 2222, and `*` to every host +and port. The host is never resolved: an endpoint configured with an address only uses the entries for +that address, and an IPv6 address such as `::1` is not expanded. +* The server is accepted when an entry of its host lists the key it presents. A host can be listed +with several keys, one per line. +* The server is rejected when the key it presents is listed in a matching `@revoked` entry, or when +its host has entries without a marker but none of them lists that key, whatever the value of +`failOnUnknownHost`. That option only decides for a host without such an entry: `@revoked` and +`@cert-authority` entries do not list a host. +* `@cert-authority` entries (OpenSSH host certificates) are not supported and are ignored. +* A `known_hosts` resource that cannot be read, or that has a line that cannot be read (for example +an unknown `@` marker, a missing key, or a key that is not valid base64), makes every connection fail. + +The following route verifies the host key of a server listening on port 2222 against a +`known_hosts` file that contains the line `[db.example.com]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...`. + +[tabs] +==== +Java:: ++ +[source,java] +---- +from("direct:status") + .to("ssh://scott@db.example.com:2222?certResource=classpath:test_rsa&knownHostsResource=file:/etc/camel/known_hosts&failOnUnknownHost=true"); +---- + +XML:: ++ +[source,xml] +---- + + + + +---- + +YAML:: ++ +[source,yaml] +---- +- route: + from: + uri: direct:status + steps: + - to: + uri: ssh://scott@db.example.com:2222 + parameters: + certResource: "classpath:test_rsa" + knownHostsResource: "file:/etc/camel/known_hosts" + failOnUnknownHost: true +---- +==== + [[SSH-CertificateDependencies]] === Certificate Dependencies diff --git a/components/camel-ssh/src/generated/resources/META-INF/org/apache/camel/component/ssh/ssh.json b/components/camel-ssh/src/generated/resources/META-INF/org/apache/camel/component/ssh/ssh.json index 48513dab7dff8..dd1c9ac5740bf 100644 --- a/components/camel-ssh/src/generated/resources/META-INF/org/apache/camel/component/ssh/ssh.json +++ b/components/camel-ssh/src/generated/resources/META-INF/org/apache/camel/component/ssh/ssh.json @@ -47,11 +47,11 @@ "certResource": { "index": 19, "kind": "property", "displayName": "Cert Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path of the certificate to use for Authentication. Will use ResourceHelperKeyPairProvider to resolve file based certificate, and depends on keyType setting." }, "certResourcePassword": { "index": 20, "kind": "property", "displayName": "Cert Resource Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the password to use in loading certResource, if certResource is an encrypted key." }, "ciphers": { "index": 21, "kind": "property", "displayName": "Ciphers", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported ciphers in their order of preference." }, - "failOnUnknownHost": { "index": 22, "kind": "property", "displayName": "Fail On Unknown Host", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "false", "defaultValue": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Specifies whether a connection to an unknown SSH server host key should fail. When false, host keys that are not present in the known_hosts resource are accepted." }, + "failOnUnknownHost": { "index": 22, "kind": "property", "displayName": "Fail On Unknown Host", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "false", "defaultValue": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Specifies whether a connection to a server whose host has no entry without a marker in the known_hosts resource should fail. When false, such a server is accepted. A server whose host has entries without a marker is always rejected when it presents a key that none of them lists, and a key listed in a matching revoked entry is always rejected." }, "kex": { "index": 23, "kind": "property", "displayName": "Kex", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported key exchange algorithms in their order of preference." }, "keyPairProvider": { "index": 24, "kind": "property", "displayName": "Key Pair Provider", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.sshd.common.keyprovider.KeyPairProvider", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the KeyPairProvider reference to use when connecting using Certificates to the remote SSH Server." }, "keyType": { "index": 25, "kind": "property", "displayName": "Key Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the key type to pass to the KeyPairProvider as part of authentication. KeyPairProvider.loadKey(...) will be passed this value. From Camel 3.0.0 \/ 2.25.0, by default Camel will select the first available KeyPair that is loaded. Prior to this, a KeyType of 'ssh-rsa' was enforced by default." }, - "knownHostsResource": { "index": 26, "kind": "property", "displayName": "Known Hosts Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path for a known_hosts file used to verify the SSH server host key. When not set, the client does not verify the server host key against a known_hosts file." }, + "knownHostsResource": { "index": 26, "kind": "property", "displayName": "Known Hosts Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path for a known_hosts file used to verify the SSH server host key. The file uses the OpenSSH known_hosts format, and its entries are matched against the host and port configured on the endpoint. A resource that cannot be read, or that has a line that cannot be read, makes every connection fail. When not set, the client does not verify the server host key against a known_hosts file." }, "macs": { "index": 27, "kind": "property", "displayName": "Macs", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported message authentication code algorithms in their order of preference. The MAC algorithm is used for data integrity protection." }, "password": { "index": 28, "kind": "property", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the password to use in connecting to remote SSH server. Requires keyPairProvider to be set to null." }, "signatures": { "index": 29, "kind": "property", "displayName": "Signatures", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported signature algorithms in their order of preference." }, @@ -102,11 +102,11 @@ "certResource": { "index": 35, "kind": "parameter", "displayName": "Cert Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path of the certificate to use for Authentication. Will use ResourceHelperKeyPairProvider to resolve file based certificate, and depends on keyType setting." }, "certResourcePassword": { "index": 36, "kind": "parameter", "displayName": "Cert Resource Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the password to use in loading certResource, if certResource is an encrypted key." }, "ciphers": { "index": 37, "kind": "parameter", "displayName": "Ciphers", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported ciphers in their order of preference." }, - "failOnUnknownHost": { "index": 38, "kind": "parameter", "displayName": "Fail On Unknown Host", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "false", "defaultValue": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Specifies whether a connection to an unknown SSH server host key should fail. When false, host keys that are not present in the known_hosts resource are accepted." }, + "failOnUnknownHost": { "index": 38, "kind": "parameter", "displayName": "Fail On Unknown Host", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "false", "defaultValue": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Specifies whether a connection to a server whose host has no entry without a marker in the known_hosts resource should fail. When false, such a server is accepted. A server whose host has entries without a marker is always rejected when it presents a key that none of them lists, and a key listed in a matching revoked entry is always rejected." }, "kex": { "index": 39, "kind": "parameter", "displayName": "Kex", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported key exchange algorithms in their order of preference." }, "keyPairProvider": { "index": 40, "kind": "parameter", "displayName": "Key Pair Provider", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.sshd.common.keyprovider.KeyPairProvider", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the KeyPairProvider reference to use when connecting using Certificates to the remote SSH Server." }, "keyType": { "index": 41, "kind": "parameter", "displayName": "Key Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the key type to pass to the KeyPairProvider as part of authentication. KeyPairProvider.loadKey(...) will be passed this value. From Camel 3.0.0 \/ 2.25.0, by default Camel will select the first available KeyPair that is loaded. Prior to this, a KeyType of 'ssh-rsa' was enforced by default." }, - "knownHostsResource": { "index": 42, "kind": "parameter", "displayName": "Known Hosts Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path for a known_hosts file used to verify the SSH server host key. When not set, the client does not verify the server host key against a known_hosts file." }, + "knownHostsResource": { "index": 42, "kind": "parameter", "displayName": "Known Hosts Resource", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:ssl", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the resource path for a known_hosts file used to verify the SSH server host key. The file uses the OpenSSH known_hosts format, and its entries are matched against the host and port configured on the endpoint. A resource that cannot be read, or that has a line that cannot be read, makes every connection fail. When not set, the client does not verify the server host key against a known_hosts file." }, "macs": { "index": 43, "kind": "parameter", "displayName": "Macs", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported message authentication code algorithms in their order of preference. The MAC algorithm is used for data integrity protection." }, "password": { "index": 44, "kind": "parameter", "displayName": "Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Sets the password to use in connecting to remote SSH server. Requires keyPairProvider to be set to null." }, "signatures": { "index": 45, "kind": "parameter", "displayName": "Signatures", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.ssh.SshConfiguration", "configurationField": "configuration", "description": "Comma-separated list of allowed\/supported signature algorithms in their order of preference." }, diff --git a/components/camel-ssh/src/main/docs/ssh-component.adoc b/components/camel-ssh/src/main/docs/ssh-component.adoc index 698ec5f305daa..e1b2e38de453a 100644 --- a/components/camel-ssh/src/main/docs/ssh-component.adoc +++ b/components/camel-ssh/src/main/docs/ssh-component.adoc @@ -163,6 +163,72 @@ YAML:: An example of using Public Key authentication is provided in `examples/camel-example-ssh-security`. +=== Server host key verification + +The SSH Component verifies the host key of the SSH server only when the `knownHostsResource` option +is set. It refers to a file in the OpenSSH `known_hosts` format, such as the output of `ssh-keyscan`. +Set `failOnUnknownHost` to `true` as well, so that a server without any entry in that file is rejected. +When it is `false` (the default), such a server is accepted and a warning is logged. + +The entries are matched as OpenSSH matches them: + +* Each host pattern of an entry is compared, ignoring case, with the host name or address the client +connects to: the host configured on the endpoint, or the `HostName` that `~/.ssh/config` gives for it. +It is written `host` for port 22 and `[host]:port` for another port. `*` and `?` are wildcards, a +pattern starting with `!` excludes the hosts it matches, and hashed entries are supported. So +`db.example.com` only applies to port 22, `[db.example.com]:2222` to port 2222, and `*` to every host +and port. The host is never resolved: an endpoint configured with an address only uses the entries for +that address, and an IPv6 address such as `::1` is not expanded. +* The server is accepted when an entry of its host lists the key it presents. A host can be listed +with several keys, one per line. +* The server is rejected when the key it presents is listed in a matching `@revoked` entry, or when +its host has entries without a marker but none of them lists that key, whatever the value of +`failOnUnknownHost`. That option only decides for a host without such an entry: `@revoked` and +`@cert-authority` entries do not list a host. +* `@cert-authority` entries (OpenSSH host certificates) are not supported and are ignored. +* A `known_hosts` resource that cannot be read, or that has a line that cannot be read (for example +an unknown `@` marker, a missing key, or a key that is not valid base64), makes every connection fail. + +The following route verifies the host key of a server listening on port 2222 against a +`known_hosts` file that contains the line `[db.example.com]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5...`. + +[tabs] +==== +Java:: ++ +[source,java] +---- +from("direct:status") + .to("ssh://scott@db.example.com:2222?certResource=classpath:test_rsa&knownHostsResource=file:/etc/camel/known_hosts&failOnUnknownHost=true"); +---- + +XML:: ++ +[source,xml] +---- + + + + +---- + +YAML:: ++ +[source,yaml] +---- +- route: + from: + uri: direct:status + steps: + - to: + uri: ssh://scott@db.example.com:2222 + parameters: + certResource: "classpath:test_rsa" + knownHostsResource: "file:/etc/camel/known_hosts" + failOnUnknownHost: true +---- +==== + [[SSH-CertificateDependencies]] === Certificate Dependencies diff --git a/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifier.java b/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifier.java index 1b5556466d72e..73bd032342bb1 100644 --- a/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifier.java +++ b/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifier.java @@ -18,30 +18,66 @@ import java.io.IOException; import java.io.InputStream; -import java.math.BigInteger; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.security.NoSuchAlgorithmException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; import java.util.Base64; -import java.util.LinkedList; -import java.util.List; +import java.util.Collections; +import java.util.Locale; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; import org.apache.camel.CamelContext; import org.apache.camel.support.ResourceHelper; -import org.apache.camel.util.IOHelper; +import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.session.ClientSession; -import org.bouncycastle.util.Arrays; +import org.apache.sshd.client.session.ClientSessionCreator; +import org.apache.sshd.common.AttributeRepository; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntry; +import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.net.SshdSocketAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * ServerKeyVerifier that takes a camel resource as input file to validate the server key against. - * + *

+ * The resource uses the OpenSSH known_hosts format, and its entries are matched as OpenSSH matches them. Each host + * pattern of an entry is compared, ignoring case, with the host name or address the client connects to (the host + * configured on the endpoint, or the {@code HostName} that {@code ~/.ssh/config} gives for it, as it is written and + * never resolved through DNS, so an IPv6 address such as {@code ::1} is not expanded), written {@code host} for port 22 + * and {@code [host]:port} for another port: {@code *} and {@code ?} are wildcards, a pattern starting with {@code !} + * excludes the hosts it matches, and hashed entries are supported. The server is accepted when an entry of its host + * lists the key it presents. It is rejected when that key is listed in a matching {@code @revoked} entry, or when its + * host has entries without a marker but none of them lists that key. {@code failOnUnknownHost} only decides for a host + * without such an entry: {@code @revoked} and {@code @cert-authority} entries do not list a host, and host certificates + * are not supported. A resource that cannot be read, or that has a line that cannot be read, rejects every server. */ public class ResourceBasedSSHKeyVerifier implements ServerKeyVerifier { + private static final int DEFAULT_PORT = 22; + private static final String MARKER_REVOKED = "@revoked"; + private static final String MARKER_CERT_AUTHORITY = "@cert-authority"; + private static final String HASHED_HOST_PREFIX = "|1|"; + private static final String HASH_ALGORITHM = "HmacSHA1"; + private static final int HASH_LENGTH = 20; + + /** + * The host configuration that the client connected with, which the ssh producer and consumer put in the connection + * context of the session: the entries are matched against its {@code HostName} as it is written. Without it, the + * address the client connected to is used, which writes an IPv6 address such as {@code ::1} in its full form. + * + * @since 4.24 + */ + public static final AttributeRepository.AttributeKey HOST_CONFIG_ENTRY + = new AttributeRepository.AttributeKey<>(); + protected final Logger log = LoggerFactory.getLogger(getClass()); private CamelContext camelContext; @@ -62,21 +98,58 @@ public ResourceBasedSSHKeyVerifier(CamelContext camelContext, String knownHostsR @Override public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) { log.debug("Trying to find known_hosts file {}", knownHostsResource); - InputStream knownHostsInputStream = null; - try { - knownHostsInputStream = ResourceHelper.resolveMandatoryResourceAsInputStream(camelContext, - knownHostsResource); - List possibleTokens = getKnownHostsFileTokensForSocketAddress(remoteAddress); - log.debug("Trying to match PublicKey against provided known_hosts file"); - PublicKey matchingKey = findKeyForServerToken(knownHostsInputStream, possibleTokens); - if (matchingKey != null) { - log.debug("Found PublicKey match for server"); - return Arrays.areEqual(matchingKey.getEncoded(), serverKey.getEncoded()); - } + String knownHosts; + try (InputStream knownHostsInputStream + = ResourceHelper.resolveMandatoryResourceAsInputStream(camelContext, knownHostsResource)) { + knownHosts = new String(knownHostsInputStream.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException ioException) { - log.debug(String.format("Could not find known_hosts file %s", knownHostsResource), ioException); - } finally { - IOHelper.close(knownHostsInputStream); + log.warn("Could not read the known_hosts file {}, connection will fail", knownHostsResource, ioException); + return false; + } + + String server = toKnownHostsName(sshClientSession, remoteAddress); + boolean hostListed = false; + boolean keyListed = false; + int lineNumber = 0; + for (String line : knownHosts.split("\\R")) { + lineNumber++; + String text = line.trim(); + if (text.isEmpty() || text.charAt(0) == '#') { + continue; + } + // a line that cannot be read could list the server, so it cannot be skipped + KnownHostsEntry entry = KnownHostsEntry.parse(text, sshClientSession); + if (entry == null) { + log.warn("Line {} of the known_hosts file {} cannot be read, connection will fail", lineNumber, + knownHostsResource); + return false; + } + if (server == null || MARKER_CERT_AUTHORITY.equals(entry.marker) || !entry.matches(server)) { + continue; + } + boolean sameKey = isSameKey(entry, serverKey); + if (MARKER_REVOKED.equals(entry.marker)) { + if (sameKey) { + log.warn("The key presented by {} is marked as revoked in the known_hosts file {}", server, + knownHostsResource); + return false; + } + } else { + hostListed = true; + if (sameKey) { + keyListed = true; + } + } + } + + if (keyListed) { + log.debug("Found PublicKey match for server {}", server); + return true; + } + if (hostListed) { + log.warn("The key presented by {} does not match any key listed for it in the known_hosts file {}", server, + knownHostsResource); + return false; } if (failOnUnknownHost) { log.warn("Could not find matching key for client session, connection will fail due to configuration"); @@ -88,105 +161,164 @@ public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress rem } } - private PublicKey findKeyForServerToken(InputStream knownHostsInputStream, List possibleTokens) { - String knowHostsLines = camelContext.getTypeConverter().convertTo(String.class, knownHostsInputStream); - if (knowHostsLines == null) { - log.warn("Could not read from the known_hosts file input stream"); - return null; + private boolean isSameKey(KnownHostsEntry entry, PublicKey serverKey) { + if (entry.key == null) { + log.warn("Unsupported key type {} in the known_hosts file {}", entry.keyType, knownHostsResource); + return false; } + return KeyUtils.compareKeys(entry.key, serverKey); + } - for (String s : knowHostsLines.split("\n")) { - String[] parts = s.split(" "); - if (parts.length != 3) { - log.warn("Found malformed entry in known_hosts file"); - continue; - } - String entry = parts[0]; - String key = parts[2]; - for (String serverToken : possibleTokens) { - if (entry.contains(serverToken)) { - try { - return loadKey(key); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - log.warn(String.format("Could not load key for server token %s", entry), e); - } - } - } + /* + * The name OpenSSH looks the server up by: the host name or address the client connected to, as the host + * configuration writes it and never resolved through DNS, with the port the client connected to, written host for + * port 22 and [host]:port for another port. The connected address cannot give the host as it is written, as it + * writes an IPv6 address such as ::1 in its full form. + */ + private static String toKnownHostsName(ClientSession session, SocketAddress remoteAddress) { + if (!(remoteAddress instanceof InetSocketAddress) && !(remoteAddress instanceof SshdSocketAddress)) { + return null; + } + SshdSocketAddress address = SshdSocketAddress.toSshdSocketAddress(remoteAddress); + String name = address.getHostName(); + SshdSocketAddress target = session != null ? session.getAttribute(ClientSessionCreator.TARGET_SERVER) : null; + AttributeRepository context = session != null ? session.getConnectionContext() : null; + HostConfigEntry hostConfig = context != null ? context.getAttribute(HOST_CONFIG_ENTRY) : null; + if (target != null) { + // a host reached through a jump host, whose HostName SSHD keeps as it is written + name = target.getHostName(); + } else if (hostConfig != null && (hostConfig.getProxyJump() == null || hostConfig.getProxyJump().isEmpty())) { + // the host the client connected to directly, with the host configuration it connected with + name = hostConfig.getHostName(); } - return null; + if (name.length() > 1 && name.charAt(0) == '[' && name.charAt(name.length() - 1) == ']') { + // an IPv6 address in brackets + name = name.substring(1, name.length() - 1); + } + name = name.toLowerCase(Locale.ROOT); + return address.getPort() == DEFAULT_PORT ? name : "[" + name + "]:" + address.getPort(); } - private List getKnownHostsFileTokensForSocketAddress(SocketAddress remoteAddress) { - List returnList = new LinkedList<>(); - if (remoteAddress instanceof InetSocketAddress) { - InetSocketAddress inetSocketAddress = (InetSocketAddress) remoteAddress; - - String hostName = inetSocketAddress.getHostName(); - String ipAddress = inetSocketAddress.getAddress().getHostAddress(); - String remotePort = String.valueOf(inetSocketAddress.getPort()); - - returnList.add(hostName); - returnList.add("[" + hostName + "]:" + remotePort); - returnList.add(ipAddress); - returnList.add("[" + ipAddress + "]:" + remotePort); + /* + * Matches a pattern where * stands for any sequence of characters and ? for any single character. + */ + static boolean matchesPattern(String pattern, String name) { + int p = 0; + int n = 0; + int star = -1; + int resume = 0; + while (n < name.length()) { + if (p < pattern.length() && (pattern.charAt(p) == '?' || pattern.charAt(p) == name.charAt(n))) { + p++; + n++; + } else if (p < pattern.length() && pattern.charAt(p) == '*') { + star = p++; + resume = n; + } else if (star >= 0) { + p = star + 1; + n = ++resume; + } else { + return false; + } } - - return returnList; + while (p < pattern.length() && pattern.charAt(p) == '*') { + p++; + } + return p == pattern.length(); } /* - * Decode the public key string, which is a base64 encoded string that consists - * of multiple parts: 1. public key type (ssh-rsa, ssh-dss, ...) 2. binary key - * data (May consists of multiple parts) - * - * Each part is composed by two sub-parts 1. Length of the part (4 bytes) 2. - * Binary part (length as defined by 1.) - * - * Uses SSHPublicKeyHolder to construct the actual PublicKey Object - * - * Note: Currently only supports RSA and DSA Public keys as required by - * https://tools.ietf.org/html/rfc4253#section-6.6 - * + * A line of the known_hosts file: an optional marker, the host patterns or a hashed host, and the key. Anything + * after the key is a comment. */ - PublicKey loadKey(String key) throws NoSuchAlgorithmException, InvalidKeySpecException { - SSHPublicKeyHolder sshPublicKeyHolder = new SSHPublicKeyHolder(); - - byte[] keyByteArray = Base64.getDecoder().decode(key); - int keyByteArrayCursor = 0; - - byte[] tmpData = new byte[4]; - int tmpCursor = 0; - - boolean getLengthMode = true; - while (keyByteArrayCursor < keyByteArray.length) { - if (getLengthMode) { - if (tmpCursor < 4) { - tmpData[tmpCursor] = keyByteArray[keyByteArrayCursor]; - tmpCursor++; - keyByteArrayCursor++; - continue; - } else { - tmpCursor = 0; - getLengthMode = false; - tmpData = new byte[byteArrayToInt(tmpData)]; + private static final class KnownHostsEntry { + private final String marker; + private final String hosts; + private final byte[] salt; + private final byte[] hash; + private final String keyType; + private final PublicKey key; + + private KnownHostsEntry(String marker, String hosts, byte[] salt, byte[] hash, String keyType, PublicKey key) { + this.marker = marker; + this.hosts = hosts; + this.salt = salt; + this.hash = hash; + this.keyType = keyType; + this.key = key; + } + + /* + * Returns null when the line cannot be read. A key of a type that is not supported is kept as a null key. + */ + static KnownHostsEntry parse(String line, SessionContext session) { + String[] fields = line.split("\\s+"); + int index = 0; + String marker = null; + if (fields[0].startsWith("@")) { + marker = fields[0]; + if (!MARKER_REVOKED.equals(marker) && !MARKER_CERT_AUTHORITY.equals(marker)) { + return null; } + index = 1; + } + if (fields.length < index + 3) { + return null; } - tmpData[tmpCursor] = keyByteArray[keyByteArrayCursor]; - tmpCursor++; - keyByteArrayCursor++; - if (tmpCursor == tmpData.length) { - sshPublicKeyHolder.push(tmpData); - getLengthMode = true; - tmpData = new byte[4]; - tmpCursor = 0; + String hosts = fields[index]; + try { + byte[] salt = null; + byte[] hash = null; + if (hosts.startsWith("|")) { + // a hashed host: only |1|salt|hash, with a SHA-1 salt and hash, is supported + if (!hosts.startsWith(HASHED_HOST_PREFIX)) { + return null; + } + String[] parts = hosts.substring(HASHED_HOST_PREFIX.length()).split("\\|", -1); + if (parts.length != 2) { + return null; + } + salt = Base64.getDecoder().decode(parts[0]); + hash = Base64.getDecoder().decode(parts[1]); + if (salt.length != HASH_LENGTH || hash.length != HASH_LENGTH) { + return null; + } + } + PublicKeyEntry keyEntry = PublicKeyEntry.parsePublicKeyEntry(fields[index + 1] + " " + fields[index + 2]); + PublicKey key = keyEntry.resolvePublicKey(session, Collections.emptyMap(), PublicKeyEntryResolver.IGNORING); + return new KnownHostsEntry(marker, hosts, salt, hash, keyEntry.getKeyType(), key); + } catch (IOException | GeneralSecurityException | RuntimeException e) { + return null; } } - return sshPublicKeyHolder.toPublicKey(); - } + boolean matches(String name) { + if (hash != null) { + return MessageDigest.isEqual(hash, hashOf(name)); + } + boolean matched = false; + for (String pattern : hosts.split(",")) { + boolean negated = pattern.startsWith("!"); + String hostPattern = (negated ? pattern.substring(1) : pattern).toLowerCase(Locale.ROOT); + if (!hostPattern.isEmpty() && matchesPattern(hostPattern, name)) { + if (negated) { + return false; + } + matched = true; + } + } + return matched; + } - private int byteArrayToInt(byte[] tmpData) { - return new BigInteger(tmpData).intValue(); + private byte[] hashOf(String name) { + try { + Mac mac = Mac.getInstance(HASH_ALGORITHM); + mac.init(new SecretKeySpec(salt, HASH_ALGORITHM)); + return mac.doFinal(name.getBytes(StandardCharsets.UTF_8)); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Cannot compute the hash of a known_hosts host", e); + } + } } } diff --git a/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/SshConfiguration.java b/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/SshConfiguration.java index 4a9f7bf282169..717c996213454 100644 --- a/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/SshConfiguration.java +++ b/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/SshConfiguration.java @@ -56,11 +56,16 @@ public class SshConfiguration implements Cloneable { private long timeout = 30000; @UriParam(label = "security", security = "insecure:ssl", description = "Sets the resource path for a known_hosts file used to verify the SSH server host key." + + " The file uses the OpenSSH known_hosts format, and its entries are matched against the" + + " host and port configured on the endpoint. A resource that cannot be read, or that has a" + + " line that cannot be read, makes every connection fail." + " When not set, the client does not verify the server host key against a known_hosts file.") private String knownHostsResource; @UriParam(label = "security", defaultValue = "false", security = "insecure:ssl", insecureValue = "false", - description = "Specifies whether a connection to an unknown SSH server host key should fail. When false," - + " host keys that are not present in the known_hosts resource are accepted.") + description = "Specifies whether a connection to a server whose host has no entry without a marker in" + + " the known_hosts resource should fail. When false, such a server is accepted. A server" + + " whose host has entries without a marker is always rejected when it presents a key that" + + " none of them lists, and a key listed in a matching @revoked entry is always rejected.") private boolean failOnUnknownHost; @UriParam(label = "advanced", defaultValue = Channel.CHANNEL_EXEC) private String channelType = Channel.CHANNEL_EXEC; @@ -301,8 +306,10 @@ public boolean isFailOnUnknownHost() { } /** - * Specifies whether a connection to an unknown host should fail or not. This value is only checked when the - * property knownHosts is set. + * Specifies whether a connection to a server whose host has no entry without a marker in the known_hosts resource + * should fail or not. This value is only checked when the property knownHostsResource is set. A server whose host + * has entries without a marker is always rejected when it presents a key that none of them lists, and a key listed + * in a matching @revoked entry is always rejected. * * @param failOnUnknownHost boolean flag, whether a connection to an unknown host should fail */ diff --git a/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/SshHelper.java b/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/SshHelper.java index b0703f1f30bdc..c26a4bc93fe58 100644 --- a/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/SshHelper.java +++ b/components/camel-ssh/src/main/java/org/apache/camel/component/ssh/SshHelper.java @@ -18,6 +18,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; @@ -33,12 +34,16 @@ import org.apache.sshd.client.SshClient; import org.apache.sshd.client.channel.ClientChannel; import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; import org.apache.sshd.client.future.AuthFuture; import org.apache.sshd.client.future.ConnectFuture; import org.apache.sshd.client.future.OpenFuture; import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.AttributeRepository; import org.apache.sshd.common.channel.Channel; import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.net.SshdSocketAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +54,24 @@ public final class SshHelper { private SshHelper() { } + /* + * The host configuration for the host and port, resolved as SshClient.connect(username, host, port) resolves it: the + * entry that the host config resolver of the client (by default ~/.ssh/config) gives, or else the host itself. + */ + static HostConfigEntry resolveHostConfig(SshClient client, String username, String host, int port) + throws IOException { + HostConfigEntryResolver resolver = client.getHostConfigEntryResolver(); + HostConfigEntry entry + = resolver != null ? resolver.resolveEffectiveHost(host, port, null, username, null, null) : null; + if (entry == null) { + int effectivePort = port > 0 ? port : SshConfiguration.DEFAULT_SSH_PORT; + entry = SshdSocketAddress.isIPv6Address(host) + ? new HostConfigEntry("", host, effectivePort, username, null) + : new HostConfigEntry(host, host, effectivePort, username, null); + } + return entry; + } + public static SshResult sendExecCommand(Map headers, String command, SshEndpoint endpoint, SshClient client) throws Exception { SshConfiguration configuration = endpoint.getConfiguration(); @@ -63,7 +86,11 @@ public static SshResult sendExecCommand(Map headers, String comm userName = (String) headers.get(SshConstants.USERNAME_HEADER); } - ConnectFuture connectFuture = client.connect(userName, configuration.getHost(), configuration.getPort()); + // resolve the host configuration once, and connect with it: the host key is verified against the host that this + // connection goes to, as the host configuration writes it + HostConfigEntry hostConfig = resolveHostConfig(client, userName, configuration.getHost(), configuration.getPort()); + ConnectFuture connectFuture = client.connect(hostConfig, + AttributeRepository.ofKeyValuePair(ResourceBasedSSHKeyVerifier.HOST_CONFIG_ENTRY, hostConfig), null); // wait getTimeout milliseconds for connect operation to complete connectFuture.await(configuration.getTimeout()); diff --git a/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifierTest.java b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifierTest.java index f383150e1b057..8bc5f317c6384 100644 --- a/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifierTest.java +++ b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/ResourceBasedSSHKeyVerifierTest.java @@ -16,47 +16,359 @@ */ package org.apache.camel.component.ssh; -import java.io.IOException; import java.io.InputStream; -import java.security.NoSuchAlgorithmException; +import java.lang.reflect.Array; +import java.lang.reflect.Proxy; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPairGenerator; import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.util.LinkedHashMap; -import java.util.Map; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.camel.CamelContext; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.session.ClientSessionCreator; +import org.apache.sshd.common.AttributeRepository; +import org.apache.sshd.common.config.keys.PublicKeyEntry; +import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.junit.jupiter.api.io.TempDir; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ResourceBasedSSHKeyVerifierTest { + + @TempDir + Path dir; + + private CamelContext context; + private PublicKey serverKey; + private PublicKey otherKey; + + @BeforeEach + void setUp() throws Exception { + context = new DefaultCamelContext(); + context.start(); + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(new ECGenParameterSpec("secp256r1")); + serverKey = generator.generateKeyPair().getPublic(); + otherKey = generator.generateKeyPair().getPublic(); + } + + @AfterEach + void tearDown() { + context.stop(); + } + + @Test + void acceptsTheKeyListedForTheHost() throws Exception { + assertTrue(verify(true, "db.example.com", 22, serverKey, entry("db.example.com", serverKey))); + assertTrue(verify(true, "db.example.com", 22, serverKey, entry("web.example.com,db.example.com", serverKey))); + } -public class ResourceBasedSSHKeyVerifierTest { + @Test + void doesNotUseTheEntryOfAHostWhoseNameContainsTheHostName() throws Exception { + assertFalse(verify(true, "db.example.com", 22, otherKey, entry("old-db.example.com", otherKey))); + assertFalse(verify(true, "db.example.com", 22, otherKey, entry("db.example.com.old", otherKey))); + } - public static final Logger LOG = LoggerFactory.getLogger(ResourceBasedSSHKeyVerifierTest.class); + @Test + void doesNotUseTheEntryOfAnAddressContainingTheAddress() throws Exception { + String knownHosts = entry("10.0.0.12", otherKey) + entry("10.0.0.1", serverKey); + assertFalse(verify(true, "10.0.0.1", 22, otherKey, knownHosts)); + assertTrue(verify(true, "10.0.0.1", 22, serverKey, knownHosts)); + assertFalse(verify(true, "10.0.0.1", 22, otherKey, entry("110.0.0.1", otherKey))); + } @Test - public void verifyAllTypesOfPublicKeys() throws IOException { - final Map keys = new LinkedHashMap<>(); - InputStream knownHosts = getClass().getResourceAsStream("/known_hosts_types"); - assertNotNull(knownHosts); - IoUtils.readAllLines(knownHosts).forEach(l -> { - String[] tab = l.split(" "); - if (l.startsWith("#")) { - return; + void matchesThePort() throws Exception { + String knownHosts = entry("[db.example.com]:2222", otherKey); + assertFalse(verify(true, "db.example.com", 22, otherKey, knownHosts)); + assertFalse(verify(true, "db.example.com", 222, otherKey, knownHosts)); + assertTrue(verify(true, "db.example.com", 2222, otherKey, knownHosts)); + // an entry without a port only applies to the default port + assertFalse(verify(true, "db.example.com", 2222, serverKey, entry("db.example.com", serverKey))); + } + + @Test + void matchesPatternsAgainstTheHostAndPortAsOpenSshWritesThem() throws Exception { + assertTrue(verify(true, "db.example.com", 2222, serverKey, entry("*", serverKey))); + assertTrue(verify(true, "db.example.com", 2222, serverKey, entry("[*.example.com]:2222", serverKey))); + assertFalse(verify(true, "db.example.com", 2222, serverKey, entry("*.example.com", serverKey))); + assertTrue(verify(true, "db.example.com", 2222, serverKey, entry("[db.example.com]:22?2", serverKey))); + } + + @Test + void matchesNegatedPortQualifiedPatterns() throws Exception { + String knownHosts = entry("![excluded.example.com]:2222,[db.example.com]:2222", serverKey); + for (boolean failOnUnknownHost : new boolean[] { true, false }) { + assertTrue(verify(failOnUnknownHost, "db.example.com", 2222, serverKey, knownHosts)); + assertFalse(verify(failOnUnknownHost, "db.example.com", 2222, otherKey, knownHosts)); + } + assertFalse(verify(true, "excluded.example.com", 2222, serverKey, knownHosts)); + } + + @Test + void matchesWildcardNegatedAndUpperCasePatterns() throws Exception { + assertTrue(verify(true, "db.example.com", 22, serverKey, entry("*.example.com", serverKey))); + assertTrue(verify(true, "db.example.com", 22, serverKey, entry("DB.EXAMPLE.COM", serverKey))); + String knownHosts = entry("!db.example.com,*.example.com", otherKey); + assertFalse(verify(true, "db.example.com", 22, otherKey, knownHosts)); + assertTrue(verify(true, "www.example.com", 22, otherKey, knownHosts)); + } + + @Test + void matchesHashedEntries() throws Exception { + String knownHosts = entry(hashed("db.example.com"), serverKey); + assertTrue(verify(true, "db.example.com", 22, serverKey, knownHosts)); + assertFalse(verify(true, "db.example.com", 22, otherKey, knownHosts)); + assertFalse(verify(true, "www.example.com", 22, serverKey, knownHosts)); + assertFalse(verify(true, "db.example.com", 2222, serverKey, knownHosts)); + assertTrue(verify(true, "db.example.com", 2222, serverKey, entry(hashed("[db.example.com]:2222"), serverKey))); + } + + @Test + void considersEveryEntryOfTheHost() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + PublicKey rsaKey = generator.generateKeyPair().getPublic(); + String knownHosts = entry("db.example.com", rsaKey) + entry("db.example.com", serverKey); + assertTrue(verify(true, "db.example.com", 22, serverKey, knownHosts)); + assertTrue(verify(true, "db.example.com", 22, rsaKey, knownHosts)); + } + + @Test + void readsCommentsAndCrlfLineEndings() throws Exception { + String knownHosts = "# known hosts\r\n" + "db.example.com " + PublicKeyEntry.toString(serverKey) + " db key\r\n"; + assertTrue(verify(true, "db.example.com", 22, serverKey, knownHosts)); + } + + @Test + void rejectsARevokedKey() throws Exception { + String knownHosts = "@revoked * " + PublicKeyEntry.toString(otherKey) + "\n" + entry("db.example.com", otherKey); + assertFalse(verify(true, "db.example.com", 22, otherKey, knownHosts)); + assertFalse(verify(false, "db.example.com", 22, otherKey, knownHosts)); + assertTrue(verify(true, "db.example.com", 22, serverKey, knownHosts + entry("db.example.com", serverKey))); + } + + @Test + void rejectsARevokedKeyOnEveryPortAndInEveryOrder() throws Exception { + String revoked = "@revoked * " + PublicKeyEntry.toString(serverKey) + "\n"; + for (int port : new int[] { 22, 2222 }) { + String listed = entry(port == 22 ? "db.example.com" : "[db.example.com]:" + port, serverKey); + for (boolean failOnUnknownHost : new boolean[] { true, false }) { + assertFalse(verify(failOnUnknownHost, "db.example.com", port, serverKey, revoked + listed), + () -> "revocation before the entry, port " + port + ", failOnUnknownHost " + failOnUnknownHost); + assertFalse(verify(failOnUnknownHost, "db.example.com", port, serverKey, listed + revoked), + () -> "revocation after the entry, port " + port + ", failOnUnknownHost " + failOnUnknownHost); } - keys.put(tab[1], tab[2]); - }); - - ResourceBasedSSHKeyVerifier verifier = new ResourceBasedSSHKeyVerifier(null, null); - keys.forEach((type, data) -> { - LOG.info("Checking key {}", type); - try { - PublicKey publicKey = verifier.loadKey(data); - assertNotNull(publicKey); - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new RuntimeException(e.getMessage(), e); + } + } + + @Test + void treatsAHostWithOnlyRevokedEntriesAsUnknown() throws Exception { + // as OpenSSH does: a revoked entry rejects its key but does not list the host + String knownHosts = "@revoked db.example.com " + PublicKeyEntry.toString(otherKey) + "\n"; + assertFalse(verify(true, "db.example.com", 22, serverKey, knownHosts)); + assertTrue(verify(false, "db.example.com", 22, serverKey, knownHosts)); + assertFalse(verify(false, "db.example.com", 22, otherKey, knownHosts)); + } + + @Test + void rejectsEveryServerWhenALineCannotBeRead() throws Exception { + String key = PublicKeyEntry.toString(serverKey); + String[] unreadableLines = { + "@unknown db.example.com " + key, + "@revoked", + "db.example.com", + "db.example.com ssh-ed25519 not*base64", + "|1|c2FsdA==|aGFzaA== " + key, + "|1|" + base64(20) + "| " + key, + "|1|" + base64(20) + "|" + base64(19) + " " + key, + "|1|" + base64(20) + "|" + base64(21) + " " + key, + "|2|" + base64(20) + "|" + base64(20) + " " + key }; + for (String line : unreadableLines) { + for (String knownHosts : new String[] { line + "\n", entry("db.example.com", serverKey) + line + "\n" }) { + for (boolean failOnUnknownHost : new boolean[] { true, false }) { + assertFalse(verify(failOnUnknownHost, "db.example.com", 22, serverKey, knownHosts), line); + assertFalse(verify(failOnUnknownHost, "www.example.com", 22, serverKey, knownHosts), line); + } + } + } + } + + @Test + void rejectsEveryServerWhenTheResourceCannotBeRead() { + for (boolean failOnUnknownHost : new boolean[] { true, false }) { + ResourceBasedSSHKeyVerifier verifier + = new ResourceBasedSSHKeyVerifier(context, "file:" + dir.resolve("missing"), failOnUnknownHost); + assertFalse(verifier.verifyServerKey(null, new InetSocketAddress("127.0.0.1", 22), serverKey)); + } + } + + @Test + void ignoresCertificateAuthorityEntries() throws Exception { + String knownHosts = "@cert-authority db.example.com " + PublicKeyEntry.toString(serverKey) + "\n"; + assertFalse(verify(true, "db.example.com", 22, serverKey, knownHosts)); + } + + @Test + void rejectsAnotherKeyForAListedHostWhateverFailOnUnknownHost() throws Exception { + String knownHosts = entry("10.0.0.12", otherKey) + entry("10.0.0.1", serverKey); + assertFalse(verify(false, "10.0.0.1", 22, otherKey, knownHosts)); + assertFalse(verify(false, "db.example.com", 22, otherKey, entry("db.example.com", serverKey))); + } + + @Test + void appliesFailOnUnknownHostToHostsWithoutEntries() throws Exception { + String knownHosts = entry("www.example.com", serverKey); + assertFalse(verify(true, "db.example.com", 22, serverKey, knownHosts)); + assertTrue(verify(false, "db.example.com", 22, serverKey, knownHosts)); + assertFalse(verify(true, "db.example.com", 22, serverKey, "")); + assertTrue(verify(false, "db.example.com", 22, serverKey, "")); + } + + @Test + void looksUpAnAddressAsConfigured() throws Exception { + // an endpoint configured with an address is looked up by that address, not by the name of a reverse lookup + String knownHosts = entry("localhost", otherKey) + entry("127.0.0.1", serverKey); + assertFalse(verify(true, new InetSocketAddress("127.0.0.1", 22), otherKey, knownHosts)); + assertTrue(verify(true, new InetSocketAddress("127.0.0.1", 22), serverKey, knownHosts)); + } + + @Test + void looksUpTheHostAsTheHostConfigurationOfTheConnectionWritesIt() throws Exception { + // the address the client connected to writes ::1 as 0:0:0:0:0:0:0:1 + InetSocketAddress ipv6 = new InetSocketAddress("::1", 2222); + String knownHosts = entry("[::1]:2222", serverKey); + for (String hostName : new String[] { "::1", "[::1]" }) { + // as an endpoint configured with ::1, or Host review-ipv6 / HostName ::1 in ~/.ssh/config, connects + ClientSession session = session(new HostConfigEntry("review-ipv6", hostName, 2222, "smx"), null); + for (boolean failOnUnknownHost : new boolean[] { true, false }) { + assertTrue(verify(failOnUnknownHost, session, ipv6, serverKey, knownHosts), hostName); + assertFalse(verify(failOnUnknownHost, session, ipv6, otherKey, knownHosts), hostName); } - }); + } + // Host db / HostName db.example.com in ~/.ssh/config + ClientSession db = session(new HostConfigEntry("db", "db.example.com", 22, "smx"), null); + InetSocketAddress dbAddress + = new InetSocketAddress(InetAddress.getByAddress("db.example.com", new byte[] { 10, 0, 0, 1 }), 22); + assertTrue(verify(true, db, dbAddress, serverKey, entry("db.example.com", serverKey))); + assertFalse(verify(true, db, dbAddress, serverKey, entry("db", serverKey))); + assertFalse(verify(false, db, dbAddress, otherKey, entry("db.example.com", serverKey))); + // without the host configuration of the connection, the connected address is used as it writes the host + assertFalse(verify(true, session(null, null), ipv6, serverKey, knownHosts)); + } + + @Test + void looksUpAHostReachedThroughAJumpHostAsItIsWritten() throws Exception { + HostConfigEntry viaJump = new HostConfigEntry("review-ipv6", "::1", 2222, "smx", "jump.example.com"); + // the target, which SSHD connects to through the jump host and keeps as it is written + ClientSession target = session(viaJump, new SshdSocketAddress("::1", 2222)); + InetSocketAddress ipv6 = new InetSocketAddress("::1", 2222); + assertTrue(verify(true, target, ipv6, serverKey, entry("[::1]:2222", serverKey))); + assertFalse(verify(false, target, ipv6, otherKey, entry("[::1]:2222", serverKey))); + // the jump host itself is looked up by its own address, not by the name of the target + ClientSession jump = session(viaJump, null); + InetSocketAddress jumpAddress + = new InetSocketAddress(InetAddress.getByAddress("jump.example.com", new byte[] { 10, 0, 0, 2 }), 22); + String knownHosts = entry("jump.example.com", otherKey) + entry("[::1]:2222", serverKey); + assertTrue(verify(true, jump, jumpAddress, otherKey, knownHosts)); + assertFalse(verify(true, jump, jumpAddress, serverKey, knownHosts)); + } + + @Test + void verifiesAllSupportedKeyTypes() throws Exception { + ResourceBasedSSHKeyVerifier verifier = new ResourceBasedSSHKeyVerifier(context, "classpath:known_hosts_types", true); + try (InputStream knownHosts = getClass().getResourceAsStream("/known_hosts_types")) { + assertNotNull(knownHosts); + for (String line : IoUtils.readAllLines(knownHosts)) { + String[] fields = line.split(" "); + PublicKey key = PublicKeyEntry.parsePublicKeyEntry(fields[1] + " " + fields[2]) + .resolvePublicKey(null, Collections.emptyMap(), PublicKeyEntryResolver.FAILING); + assertTrue(verifier.verifyServerKey(null, new InetSocketAddress(fields[0], 22), key), fields[1]); + assertFalse(verifier.verifyServerKey(null, new InetSocketAddress(fields[0], 22), serverKey), fields[1]); + } + } + } + + private boolean verify(boolean failOnUnknownHost, String host, int port, PublicKey presented, String knownHosts) + throws Exception { + // the SSH client hands the verifier the configured host name together with its resolved address + InetAddress address = InetAddress.getByAddress(host, new byte[] { 10, 0, 0, 1 }); + return verify(failOnUnknownHost, new InetSocketAddress(address, port), presented, knownHosts); + } + + private boolean verify( + boolean failOnUnknownHost, InetSocketAddress server, PublicKey presented, String knownHosts) + throws Exception { + return verify(failOnUnknownHost, null, server, presented, knownHosts); + } + + private boolean verify( + boolean failOnUnknownHost, ClientSession session, InetSocketAddress server, PublicKey presented, + String knownHosts) + throws Exception { + Path file = Files.createTempFile(dir, "known_hosts", ""); + Files.writeString(file, knownHosts, StandardCharsets.UTF_8); + ResourceBasedSSHKeyVerifier verifier = new ResourceBasedSSHKeyVerifier(context, "file:" + file, failOnUnknownHost); + return verifier.verifyServerKey(session, server, presented); + } + + /* + * A session connected with the given host configuration, as the ssh producer and consumer connect, and reached + * through a jump host when a target server is given: all that the verifier asks the session for. + */ + private static ClientSession session(HostConfigEntry hostConfig, SshdSocketAddress targetServer) { + AttributeRepository connectionContext = hostConfig != null + ? AttributeRepository.ofKeyValuePair(ResourceBasedSSHKeyVerifier.HOST_CONFIG_ENTRY, hostConfig) : null; + return (ClientSession) Proxy.newProxyInstance(ClientSession.class.getClassLoader(), + new Class[] { ClientSession.class }, (proxy, method, args) -> { + switch (method.getName()) { + case "getConnectionContext": + return connectionContext; + case "getAttribute": + return args[0] == ClientSessionCreator.TARGET_SERVER ? targetServer : null; + default: + Class type = method.getReturnType(); + // the default value of a primitive type, null otherwise + return type.isPrimitive() && type != void.class + ? Array.get(Array.newInstance(type, 1), 0) : null; + } + }); + } + + private static String base64(int length) { + return Base64.getEncoder().encodeToString(new byte[length]); + } + + private static String entry(String hosts, PublicKey key) { + return hosts + " " + PublicKeyEntry.toString(key) + "\n"; + } + + private static String hashed(String host) throws Exception { + byte[] salt = new byte[20]; + Arrays.fill(salt, (byte) 7); + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(new SecretKeySpec(salt, "HmacSHA1")); + byte[] hash = mac.doFinal(host.getBytes(StandardCharsets.UTF_8)); + return "|1|" + Base64.getEncoder().encodeToString(salt) + "|" + Base64.getEncoder().encodeToString(hash); } } diff --git a/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostEdDSATest.java b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostEdDSATest.java index 243610be3cad1..2fc472611aa7e 100644 --- a/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostEdDSATest.java +++ b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostEdDSATest.java @@ -22,6 +22,14 @@ public class SshComponentKnownHostEdDSATest extends SshComponentTestSupport { + private String knownHosts; + + @Override + public void doPreSetup() throws Exception { + super.doPreSetup(); + knownHosts = writeKnownHosts("known_hosts_eddsa", "[localhost]:" + port + " " + serverPublicKey()); + } + @Test public void testProducerWithEdDSAKeyType() throws Exception { final String msg = "test"; @@ -50,12 +58,13 @@ public void configure() { onException(Exception.class).handled(true).to("mock:error"); from("ssh://smx:smx@localhost:" + port - + "?useFixedDelay=true&delay=40000&pollCommand=test%0A&knownHostsResource=classpath:known_hosts_eddsa&failOnUnknownHost=true") + + "?useFixedDelay=true&delay=40000&pollCommand=test%0A&knownHostsResource=" + knownHosts + + "&failOnUnknownHost=true") .to("mock:result"); from("direct:ssh") .to("ssh://smx:smx@localhost:" + port - + "?timeout=3000&knownHostsResource=classpath:known_hosts_eddsa&failOnUnknownHost=true") + + "?timeout=3000&knownHostsResource=" + knownHosts + "&failOnUnknownHost=true") .to("mock:password"); } }; diff --git a/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostIPv6Test.java b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostIPv6Test.java new file mode 100644 index 0000000000000..45d390cec790e --- /dev/null +++ b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostIPv6Test.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.ssh; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.KeyPairGenerator; +import java.security.spec.ECGenParameterSpec; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.sshd.client.ClientBuilder; +import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; +import org.apache.sshd.common.config.keys.PublicKeyEntry; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * A server reached at the IPv6 address ::1 is looked up as [::1]:port, as the address is written, although the address + * the client connects to writes it as 0:0:0:0:0:0:0:1: both when the endpoint is configured with ::1, and when the + * endpoint is configured with an alias that the host configuration of the client (as ~/.ssh/config would) gives the + * HostName ::1 for. The host is set on the endpoint, as the endpoint uri does not accept an IPv6 address. + */ +class SshComponentKnownHostIPv6Test extends SshComponentTestSupport { + + private static final String ALIAS = "review-ipv6"; + + private String knownHostsServerKey; + private String knownHostsOtherKey; + + @Override + public void doPreSetup() throws Exception { + super.doPreSetup(); + assumeTrue(isIPv6LoopbackAvailable(), "The IPv6 loopback address is not available"); + knownHostsServerKey = writeKnownHosts("known_hosts_server_key", "[::1]:" + port + " " + serverPublicKey()); + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(new ECGenParameterSpec("secp256r1")); + String otherKey = PublicKeyEntry.toString(generator.generateKeyPair().getPublic()); + knownHostsOtherKey = writeKnownHosts("known_hosts_other_key", "[::1]:" + port + " " + otherKey); + } + + @Test + void testProducerWithTheServerKey() throws Exception { + assertDelivered("serverKey"); + } + + @Test + void testProducerWithTheServerKeyAcceptingUnknownHosts() throws Exception { + assertDelivered("serverKeyAcceptingUnknownHosts"); + } + + @Test + void testProducerWithAnotherKey() throws Exception { + assertRejected("otherKey"); + } + + @Test + void testProducerWithAnotherKeyAcceptingUnknownHosts() throws Exception { + assertRejected("otherKeyAcceptingUnknownHosts"); + } + + @Test + void testProducerWithAnAliasAndTheServerKey() throws Exception { + assertDelivered("aliasServerKey"); + } + + @Test + void testProducerWithAnAliasAndTheServerKeyAcceptingUnknownHosts() throws Exception { + assertDelivered("aliasServerKeyAcceptingUnknownHosts"); + } + + @Test + void testProducerWithAnAliasAndAnotherKey() throws Exception { + assertRejected("aliasOtherKey"); + } + + @Test + void testProducerWithAnAliasAndAnotherKeyAcceptingUnknownHosts() throws Exception { + assertRejected("aliasOtherKeyAcceptingUnknownHosts"); + } + + /* + * Each route uses its own user name, which the test server records when it receives credentials. + */ + private void assertDelivered(String route) throws Exception { + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + mock.expectedBodiesReceived("test"); + getMockEndpoint("mock:error").expectedMessageCount(0); + + template.sendBody("direct:" + route, "test"); + + MockEndpoint.assertIsSatisfied(context); + assertTrue(authenticatedUsers.contains(route), "the server should receive the credentials"); + } + + private void assertRejected(String route) throws Exception { + getMockEndpoint("mock:result").expectedMessageCount(0); + getMockEndpoint("mock:error").expectedMessageCount(1); + + template.sendBody("direct:" + route, "test"); + + MockEndpoint.assertIsSatisfied(context); + assertFalse(authenticatedUsers.contains(route), "the server should not receive any credentials"); + } + + private boolean isIPv6LoopbackAvailable() { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress("::1", port), 2000); + return true; + } catch (IOException e) { + return false; + } + } + + /* + * The host configuration of the client, as ~/.ssh/config would give it: Host review-ipv6, HostName ::1. It answers + * only once, so the host key must be checked against the host that the connection was resolved to, without asking + * the host configuration again. + */ + private static HostConfigEntryResolver resolvingOnce() { + AtomicBoolean resolved = new AtomicBoolean(); + return (host, hostPort, localAddress, username, proxyJump, attributes) -> { + if (!ALIAS.equals(host)) { + return null; + } + if (resolved.getAndSet(true)) { + throw new IOException("The host configuration answers only once"); + } + return new HostConfigEntry(host, "::1", hostPort, username); + }; + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + onException(Exception.class).handled(true).to("mock:error"); + + route("serverKey", "::1", knownHostsServerKey, true); + route("serverKeyAcceptingUnknownHosts", "::1", knownHostsServerKey, false); + route("otherKey", "::1", knownHostsOtherKey, true); + route("otherKeyAcceptingUnknownHosts", "::1", knownHostsOtherKey, false); + route("aliasServerKey", ALIAS, knownHostsServerKey, true); + route("aliasServerKeyAcceptingUnknownHosts", ALIAS, knownHostsServerKey, false); + route("aliasOtherKey", ALIAS, knownHostsOtherKey, true); + route("aliasOtherKeyAcceptingUnknownHosts", ALIAS, knownHostsOtherKey, false); + } + + private void route(String name, String host, String knownHosts, boolean failOnUnknownHost) { + SshEndpoint endpoint = context.getEndpoint("ssh://" + name + ":secret@localhost:" + port + + "?timeout=3000&knownHostsResource=" + knownHosts + + "&failOnUnknownHost=" + failOnUnknownHost, + SshEndpoint.class); + endpoint.setHost(host); + endpoint.getConfiguration() + .setClientBuilder(ClientBuilder.builder().hostConfigEntryResolver(resolvingOnce())); + from("direct:" + name).to(endpoint).to("mock:result"); + } + }; + } +} diff --git a/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostTest.java b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostTest.java index 72915c883e7b8..977e28e276053 100644 --- a/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostTest.java +++ b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentKnownHostTest.java @@ -16,12 +16,42 @@ */ package org.apache.camel.component.ssh; +import java.io.IOException; +import java.security.KeyPairGenerator; +import java.security.spec.ECGenParameterSpec; + import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.mock.MockEndpoint; +import org.apache.sshd.client.ClientBuilder; +import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.common.config.keys.PublicKeyEntry; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class SshComponentKnownHostTest extends SshComponentTestSupport { + private String knownHostsValid; + private String knownHostsOtherHost; + private String knownHostsSeveralKeys; + private String knownHostsOtherKey; + + @Override + public void doPreSetup() throws Exception { + super.doPreSetup(); + String serverKey = serverPublicKey(); + String serverEntry = "[localhost]:" + port + " " + serverKey; + knownHostsValid = writeKnownHosts("known_hosts_valid", serverEntry); + knownHostsOtherHost = writeKnownHosts("known_hosts_other_host", "old-localhost " + serverKey); + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(new ECGenParameterSpec("secp256r1")); + String otherKey = PublicKeyEntry.toString(generator.generateKeyPair().getPublic()); + knownHostsSeveralKeys + = writeKnownHosts("known_hosts_several_keys", "[localhost]:" + port + " " + otherKey, serverEntry); + knownHostsOtherKey = writeKnownHosts("known_hosts_other_key", "[localhost]:" + port + " " + otherKey); + } + @Test public void testProducerWithValidFile() throws Exception { final String msg = "test"; @@ -64,6 +94,73 @@ public void testProducerWithInvalidFileWarnOnly() throws Exception { MockEndpoint.assertIsSatisfied(context); } + @Test + void testProducerWithKeyListedForAnotherHost() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:password"); + mock.expectedMessageCount(0); + getMockEndpoint("mock:error").expectedMessageCount(1); + + template.sendBody("direct:sshOtherHost", "test"); + + MockEndpoint.assertIsSatisfied(context); + } + + @Test + void testProducerWithSeveralKeysForTheServer() throws Exception { + final String msg = "test"; + + MockEndpoint mock = getMockEndpoint("mock:password"); + mock.expectedMessageCount(1); + mock.expectedBodiesReceived(msg); + + template.sendBody("direct:sshSeveralKeys", msg); + + MockEndpoint.assertIsSatisfied(context); + } + + @Test + void testProducerWithAnAliasInTheHostConfiguration() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:password"); + mock.expectedMessageCount(1); + mock.expectedBodiesReceived("test"); + + template.sendBody("direct:aliasServerKey", "test"); + + MockEndpoint.assertIsSatisfied(context); + assertTrue(authenticatedUsers.contains("aliasServerKey")); + } + + @Test + void testProducerWithAnAliasInTheHostConfigurationAndAnotherKey() throws Exception { + getMockEndpoint("mock:password").expectedMessageCount(0); + getMockEndpoint("mock:error").expectedMessageCount(1); + + template.sendBody("direct:aliasOtherKey", "test"); + + MockEndpoint.assertIsSatisfied(context); + assertFalse(authenticatedUsers.contains("aliasOtherKey")); + } + + @Test + void testProducerWithAFailingHostConfiguration() throws Exception { + assertNotConnected("brokenAlias"); + } + + @Test + void testProducerWithAFailingHostConfigurationAcceptingUnknownHosts() throws Exception { + assertNotConnected("brokenAliasAcceptingUnknownHosts"); + } + + private void assertNotConnected(String route) throws Exception { + getMockEndpoint("mock:password").expectedMessageCount(0); + getMockEndpoint("mock:error").expectedMessageCount(1); + + template.sendBody("direct:" + route, "test"); + + MockEndpoint.assertIsSatisfied(context); + assertFalse(authenticatedUsers.contains(route)); + } + @Test public void testPollingConsumerWithValidKnownHostFile() throws Exception { MockEndpoint mock = getMockEndpoint("mock:result"); @@ -99,7 +196,8 @@ public void configure() { onException(Exception.class).handled(true).to("mock:error"); from("ssh://smx:smx@localhost:" + port - + "?useFixedDelay=true&delay=40000&pollCommand=test%0A&knownHostsResource=classpath:known_hosts_valid&failOnUnknownHost=true") + + "?useFixedDelay=true&delay=40000&pollCommand=test%0A&knownHostsResource=" + knownHostsValid + + "&failOnUnknownHost=true") .to("mock:result"); from("ssh://smx:smx@localhost:" + port @@ -112,7 +210,28 @@ public void configure() { from("direct:ssh") .to("ssh://smx:smx@localhost:" + port - + "?timeout=3000&knownHostsResource=classpath:known_hosts_valid&failOnUnknownHost=true") + + "?timeout=3000&knownHostsResource=" + knownHostsValid + "&failOnUnknownHost=true") + .to("mock:password"); + + from("direct:sshOtherHost") + .to("ssh://smx:smx@localhost:" + port + + "?timeout=3000&knownHostsResource=" + knownHostsOtherHost + "&failOnUnknownHost=true") + .to("mock:password"); + + // the host configuration of the client, as ~/.ssh/config would give it: Host db-alias, HostName localhost + from("direct:aliasServerKey").to(aliasEndpoint("aliasServerKey", knownHostsValid, true)) + .to("mock:password"); + from("direct:aliasOtherKey").to(aliasEndpoint("aliasOtherKey", knownHostsOtherKey, false)) + .to("mock:password"); + + // a host configuration that cannot resolve the host: the producer must not connect anywhere + from("direct:brokenAlias").to(brokenAliasEndpoint("brokenAlias", true)).to("mock:password"); + from("direct:brokenAliasAcceptingUnknownHosts") + .to(brokenAliasEndpoint("brokenAliasAcceptingUnknownHosts", false)).to("mock:password"); + + from("direct:sshSeveralKeys") + .to("ssh://smx:smx@localhost:" + port + + "?timeout=3000&knownHostsResource=" + knownHostsSeveralKeys + "&failOnUnknownHost=true") .to("mock:password"); from("direct:sshInvalid").to("ssh://smx:smx@localhost:" + port @@ -125,4 +244,27 @@ public void configure() { } }; } + + private SshEndpoint aliasEndpoint(String user, String knownHosts, boolean failOnUnknownHost) { + SshEndpoint endpoint = context.getEndpoint("ssh://" + user + ":secret@db-alias:" + port + + "?timeout=3000&knownHostsResource=" + knownHosts + + "&failOnUnknownHost=" + failOnUnknownHost, + SshEndpoint.class); + endpoint.getConfiguration().setClientBuilder(ClientBuilder.builder().hostConfigEntryResolver( + (host, hostPort, localAddress, username, proxyJump, attributes) -> "db-alias".equals(host) + ? new HostConfigEntry(host, "localhost", hostPort, username) : null)); + return endpoint; + } + + private SshEndpoint brokenAliasEndpoint(String user, boolean failOnUnknownHost) { + SshEndpoint endpoint = context.getEndpoint("ssh://" + user + ":secret@broken-alias:" + port + + "?timeout=3000&knownHostsResource=" + knownHostsValid + + "&failOnUnknownHost=" + failOnUnknownHost, + SshEndpoint.class); + endpoint.getConfiguration().setClientBuilder(ClientBuilder.builder().hostConfigEntryResolver( + (host, hostPort, localAddress, username, proxyJump, attributes) -> { + throw new IOException("Cannot read the host configuration"); + })); + return endpoint; + } } diff --git a/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentTestSupport.java b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentTestSupport.java index e0260028ae437..db4b23cfea6b9 100644 --- a/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentTestSupport.java +++ b/components/camel-ssh/src/test/java/org/apache/camel/component/ssh/SshComponentTestSupport.java @@ -17,15 +17,26 @@ package org.apache.camel.component.ssh; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.apache.camel.test.junit6.CamelTestSupport; +import org.apache.sshd.common.config.keys.PublicKeyEntry; import org.apache.sshd.common.keyprovider.FileKeyPairProvider; import org.apache.sshd.server.SshServer; public class SshComponentTestSupport extends CamelTestSupport { protected SshServer sshd; protected int port; + /** + * The users the test server received credentials from, a password or a public key. + */ + protected final Set authenticatedUsers = ConcurrentHashMap.newKeySet(); @Override public void doPreSetup() throws Exception { @@ -33,8 +44,14 @@ public void doPreSetup() throws Exception { sshd.setPort(0); sshd.setKeyPairProvider(new FileKeyPairProvider(Paths.get(getHostKey()))); sshd.setCommandFactory(new TestEchoCommandFactory()); - sshd.setPasswordAuthenticator((username, password, session) -> true); - sshd.setPublickeyAuthenticator((username, key, session) -> true); + sshd.setPasswordAuthenticator((username, password, session) -> { + authenticatedUsers.add(username); + return true; + }); + sshd.setPublickeyAuthenticator((username, key, session) -> { + authenticatedUsers.add(username); + return true; + }); sshd.start(); port = sshd.getPort(); } @@ -43,6 +60,25 @@ protected String getHostKey() { return "src/test/resources/hostkey.pem"; } + /** + * The public key of the test server, in the form used by a known_hosts entry. + */ + protected String serverPublicKey() throws IOException, GeneralSecurityException { + KeyPair keyPair = sshd.getKeyPairProvider().loadKeys(null).iterator().next(); + return PublicKeyEntry.toString(keyPair.getPublic()); + } + + /** + * Writes a known_hosts file for this test and returns it as a resource. The test server listens on a random port, + * so its entries must use the [localhost]:port form. + */ + protected String writeKnownHosts(String name, String... entries) throws IOException { + Path file = Paths.get("target", "known_hosts", getClass().getSimpleName(), name); + Files.createDirectories(file.getParent()); + Files.writeString(file, String.join("\n", entries) + "\n"); + return "file:" + file; + } + @Override public void doPostTearDown() throws IOException, InterruptedException { if (sshd != null) { diff --git a/components/camel-ssh/src/test/resources/known_hosts_eddsa b/components/camel-ssh/src/test/resources/known_hosts_eddsa deleted file mode 100644 index 8ccd01181c5c3..0000000000000 --- a/components/camel-ssh/src/test/resources/known_hosts_eddsa +++ /dev/null @@ -1 +0,0 @@ -127.0.0.1 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGY16PbGw2BjMtxaUUWBYK9TKVeavaOXG2FvlAEe80x8 diff --git a/components/camel-ssh/src/test/resources/known_hosts_valid b/components/camel-ssh/src/test/resources/known_hosts_valid deleted file mode 100644 index e7ae1daf75e11..0000000000000 --- a/components/camel-ssh/src/test/resources/known_hosts_valid +++ /dev/null @@ -1 +0,0 @@ -127.0.0.1 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDdfIWeSV4o68dRrKSzFd/Bk51E65UTmmSrmW0O1ohtzi6HzsDPjXgCtlTt3FqTcfFfI92IlTr4JWqC9UK1QT1ZTeng0MkPQmv68hDANHbt5CpETZHjW5q4OOgWhVvj5IyOC2NZHtKlJBkdsMAa15ouOOJLzBvAvbqOR/yUROsEiQ== \ No newline at end of file diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc index 8c3dc42817e1a..82a105736ec18 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc @@ -37,6 +37,35 @@ A saga option named like a parameter of the participant callback URLs, in any ca `Camel-Saga-Complete`, `Camel-Saga-Signature` or `Camel-Saga-Lra`), now fails the creation of its route, with or without a callback secret. The value of such an option could not be read back from the callback before. +=== camel-ssh - known_hosts entries are matched as in OpenSSH + +When `knownHostsResource` is set, the entries of the `known_hosts` file are now matched as OpenSSH +matches them, against the host name or address the client connects to, written `host` for port 22 and +`[host]:port` for another port: + +* A host pattern must match that name as a whole. An entry for another host whose name or address +contains the configured one is no longer used. +* A pattern such as `db.example.com` only applies to port 22. A server on another port needs an entry +such as `[db.example.com]:2222`, or a pattern that matches that form, such as `*`. +* The host is used as it is written: the host configured on the endpoint, or the `HostName` that +`~/.ssh/config` gives for it. An endpoint configured with a host name no longer uses entries listed +only under its address, an endpoint configured with an address no longer uses entries listed under the +name that a reverse lookup of the address returns, and an IPv6 address such as `::1` is matched as +written instead of in its expanded form. +* Every entry of the host is considered, so a host can be listed with several keys. Wildcard, negated +and hashed patterns, comments and `@revoked` entries are supported. `@cert-authority` entries are +ignored. +* A `known_hosts` resource that cannot be read, or that has a line that cannot be read (for example an +unknown `@` marker, a missing key, or a key that is not valid base64), now makes every connection fail, +whatever the value of `failOnUnknownHost`. Previously such a resource was treated as having no entry, +and such a line was skipped. + +As before, a server whose host has entries without a marker is rejected when it presents a key that +none of them lists, whatever the value of `failOnUnknownHost`, which only applies to a host without +such an entry: `@revoked` and `@cert-authority` entries do not list a host. +Check that the `known_hosts` file has an entry for the host name or address, and the port, that each +endpoint is configured with. + === camel-netty-http - the roles of a security constraint are matched by role name The roles of a `SecurityConstraintMapping` inclusion are a comma-separated list of role names, and an authenticated diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index df13ee119eb9a..35fb4b7072027 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -27,6 +27,35 @@ when `enableSniffer` is set, the nodes the sniffer finds, matching the scheme, h registered for any host. A request to another host, including the host of a node with another scheme or port, is now sent without them. A client set with the `restClient` option is not changed. +=== camel-ssh - known_hosts entries are matched as in OpenSSH + +When `knownHostsResource` is set, the entries of the `known_hosts` file are now matched as OpenSSH +matches them, against the host name or address the client connects to, written `host` for port 22 and +`[host]:port` for another port: + +* A host pattern must match that name as a whole. An entry for another host whose name or address +contains the configured one is no longer used. +* A pattern such as `db.example.com` only applies to port 22. A server on another port needs an entry +such as `[db.example.com]:2222`, or a pattern that matches that form, such as `*`. +* The host is used as it is written: the host configured on the endpoint, or the `HostName` that +`~/.ssh/config` gives for it. An endpoint configured with a host name no longer uses entries listed +only under its address, an endpoint configured with an address no longer uses entries listed under the +name that a reverse lookup of the address returns, and an IPv6 address such as `::1` is matched as +written instead of in its expanded form. +* Every entry of the host is considered, so a host can be listed with several keys. Wildcard, negated +and hashed patterns, comments and `@revoked` entries are supported. `@cert-authority` entries are +ignored. +* A `known_hosts` resource that cannot be read, or that has a line that cannot be read (for example an +unknown `@` marker, a missing key, or a key that is not valid base64), now makes every connection fail, +whatever the value of `failOnUnknownHost`. Previously such a resource was treated as having no entry, +and such a line was skipped. + +As before, a server whose host has entries without a marker is rejected when it presents a key that +none of them lists, whatever the value of `failOnUnknownHost`, which only applies to a host without +such an entry: `@revoked` and `@cert-authority` entries do not list a host. +Check that the `known_hosts` file has an entry for the host name or address, and the port, that each +endpoint is configured with. + === camel-opensearch - the credentials are only sent to the nodes of the cluster The `user` and `password` are now offered only to the nodes the client talks to: the hosts of `hostAddresses` and, diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index 5b1e7e99e39d9..e2d85c0629b4a 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -874,6 +874,35 @@ as `Failed to connect, because a certificate or security check failed`. To resto The certificate that the user name and password are encrypted with is checked by the same validator. Endpoints that use the security policy `None` are not affected. +=== camel-ssh - known_hosts entries are matched as in OpenSSH + +When `knownHostsResource` is set, the entries of the `known_hosts` file are now matched as OpenSSH +matches them, against the host name or address the client connects to, written `host` for port 22 and +`[host]:port` for another port: + +* A host pattern must match that name as a whole. An entry for another host whose name or address +contains the configured one is no longer used. +* A pattern such as `db.example.com` only applies to port 22. A server on another port needs an entry +such as `[db.example.com]:2222`, or a pattern that matches that form, such as `*`. +* The host is used as it is written: the host configured on the endpoint, or the `HostName` that +`~/.ssh/config` gives for it. An endpoint configured with a host name no longer uses entries listed +only under its address, an endpoint configured with an address no longer uses entries listed under the +name that a reverse lookup of the address returns, and an IPv6 address such as `::1` is matched as +written instead of in its expanded form. +* Every entry of the host is considered, so a host can be listed with several keys. Wildcard, negated +and hashed patterns, comments and `@revoked` entries are supported. `@cert-authority` entries are +ignored. +* A `known_hosts` resource that cannot be read, or that has a line that cannot be read (for example an +unknown `@` marker, a missing key, or a key that is not valid base64), now makes every connection fail, +whatever the value of `failOnUnknownHost`. Previously such a resource was treated as having no entry, +and such a line was skipped. + +As before, a server whose host has entries without a marker is rejected when it presents a key that +none of them lists, whatever the value of `failOnUnknownHost`, which only applies to a host without +such an entry: `@revoked` and `@cert-authority` entries do not list a host. +Check that the `known_hosts` file has an entry for the host name or address, and the port, that each +endpoint is configured with. + === camel-zipfile - ZipAggregationStrategy keeps entries with the same name `ZipAggregationStrategy` replaced an entry of the zip file when a later message had the same entry name, for example diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SshComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SshComponentBuilderFactory.java index 19243cffd3263..3665cc88fc718 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SshComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/SshComponentBuilderFactory.java @@ -445,9 +445,12 @@ default SshComponentBuilder ciphers(java.lang.String ciphers) { /** - * Specifies whether a connection to an unknown SSH server host key - * should fail. When false, host keys that are not present in the - * known_hosts resource are accepted. + * Specifies whether a connection to a server whose host has no entry + * without a marker in the known_hosts resource should fail. When false, + * such a server is accepted. A server whose host has entries without a + * marker is always rejected when it presents a key that none of them + * lists, and a key listed in a matching revoked entry is always + * rejected. * * The option is a: <code>boolean</code> type. * @@ -516,8 +519,11 @@ default SshComponentBuilder keyType(java.lang.String keyType) { /** * Sets the resource path for a known_hosts file used to verify the SSH - * server host key. When not set, the client does not verify the server - * host key against a known_hosts file. + * server host key. The file uses the OpenSSH known_hosts format, and + * its entries are matched against the host and port configured on the + * endpoint. A resource that cannot be read, or that has a line that + * cannot be read, makes every connection fail. When not set, the client + * does not verify the server host key against a known_hosts file. * * The option is a: <code>java.lang.String</code> type. * diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SshEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SshEndpointBuilderFactory.java index e83926e90b7f5..c12f8d86ca9df 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SshEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SshEndpointBuilderFactory.java @@ -628,9 +628,12 @@ default SshEndpointConsumerBuilder ciphers(String ciphers) { return this; } /** - * Specifies whether a connection to an unknown SSH server host key - * should fail. When false, host keys that are not present in the - * known_hosts resource are accepted. + * Specifies whether a connection to a server whose host has no entry + * without a marker in the known_hosts resource should fail. When false, + * such a server is accepted. A server whose host has entries without a + * marker is always rejected when it presents a key that none of them + * lists, and a key listed in a matching revoked entry is always + * rejected. * * The option is a: boolean type. * @@ -645,9 +648,12 @@ default SshEndpointConsumerBuilder failOnUnknownHost(boolean failOnUnknownHost) return this; } /** - * Specifies whether a connection to an unknown SSH server host key - * should fail. When false, host keys that are not present in the - * known_hosts resource are accepted. + * Specifies whether a connection to a server whose host has no entry + * without a marker in the known_hosts resource should fail. When false, + * such a server is accepted. A server whose host has entries without a + * marker is always rejected when it presents a key that none of them + * lists, and a key listed in a matching revoked entry is always + * rejected. * * The option will be converted to a boolean type. * @@ -728,8 +734,11 @@ default SshEndpointConsumerBuilder keyType(String keyType) { } /** * Sets the resource path for a known_hosts file used to verify the SSH - * server host key. When not set, the client does not verify the server - * host key against a known_hosts file. + * server host key. The file uses the OpenSSH known_hosts format, and + * its entries are matched against the host and port configured on the + * endpoint. A resource that cannot be read, or that has a line that + * cannot be read, makes every connection fail. When not set, the client + * does not verify the server host key against a known_hosts file. * * The option is a: java.lang.String type. * @@ -1344,9 +1353,12 @@ default SshEndpointProducerBuilder ciphers(String ciphers) { return this; } /** - * Specifies whether a connection to an unknown SSH server host key - * should fail. When false, host keys that are not present in the - * known_hosts resource are accepted. + * Specifies whether a connection to a server whose host has no entry + * without a marker in the known_hosts resource should fail. When false, + * such a server is accepted. A server whose host has entries without a + * marker is always rejected when it presents a key that none of them + * lists, and a key listed in a matching revoked entry is always + * rejected. * * The option is a: boolean type. * @@ -1361,9 +1373,12 @@ default SshEndpointProducerBuilder failOnUnknownHost(boolean failOnUnknownHost) return this; } /** - * Specifies whether a connection to an unknown SSH server host key - * should fail. When false, host keys that are not present in the - * known_hosts resource are accepted. + * Specifies whether a connection to a server whose host has no entry + * without a marker in the known_hosts resource should fail. When false, + * such a server is accepted. A server whose host has entries without a + * marker is always rejected when it presents a key that none of them + * lists, and a key listed in a matching revoked entry is always + * rejected. * * The option will be converted to a boolean type. * @@ -1444,8 +1459,11 @@ default SshEndpointProducerBuilder keyType(String keyType) { } /** * Sets the resource path for a known_hosts file used to verify the SSH - * server host key. When not set, the client does not verify the server - * host key against a known_hosts file. + * server host key. The file uses the OpenSSH known_hosts format, and + * its entries are matched against the host and port configured on the + * endpoint. A resource that cannot be read, or that has a line that + * cannot be read, makes every connection fail. When not set, the client + * does not verify the server host key against a known_hosts file. * * The option is a: java.lang.String type. * @@ -1953,9 +1971,12 @@ default SshEndpointBuilder ciphers(String ciphers) { return this; } /** - * Specifies whether a connection to an unknown SSH server host key - * should fail. When false, host keys that are not present in the - * known_hosts resource are accepted. + * Specifies whether a connection to a server whose host has no entry + * without a marker in the known_hosts resource should fail. When false, + * such a server is accepted. A server whose host has entries without a + * marker is always rejected when it presents a key that none of them + * lists, and a key listed in a matching revoked entry is always + * rejected. * * The option is a: boolean type. * @@ -1970,9 +1991,12 @@ default SshEndpointBuilder failOnUnknownHost(boolean failOnUnknownHost) { return this; } /** - * Specifies whether a connection to an unknown SSH server host key - * should fail. When false, host keys that are not present in the - * known_hosts resource are accepted. + * Specifies whether a connection to a server whose host has no entry + * without a marker in the known_hosts resource should fail. When false, + * such a server is accepted. A server whose host has entries without a + * marker is always rejected when it presents a key that none of them + * lists, and a key listed in a matching revoked entry is always + * rejected. * * The option will be converted to a boolean type. * @@ -2053,8 +2077,11 @@ default SshEndpointBuilder keyType(String keyType) { } /** * Sets the resource path for a known_hosts file used to verify the SSH - * server host key. When not set, the client does not verify the server - * host key against a known_hosts file. + * server host key. The file uses the OpenSSH known_hosts format, and + * its entries are matched against the host and port configured on the + * endpoint. A resource that cannot be read, or that has a line that + * cannot be read, makes every connection fail. When not set, the client + * does not verify the server host key against a known_hosts file. * * The option is a: java.lang.String type. *