Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expanded ssh_config parameters for qemu+ssh uri option #1059

Merged
merged 32 commits into from
Sep 15, 2024

Conversation

memetb
Copy link
Contributor

@memetb memetb commented Dec 20, 2023

Hello @dmacvicar,

this PR is in relation to the issue #1058 I recently opened.

This is a preliminary PR and work in progress. I have listed the known issues with this PR at this point. I would also like to get feedback from you before putting more effort in.

This feature is a must-have for my use case since I require to access my bare metal servers through a bastion host.

Known issues:

  1. using ProxyJump with openssh implementation of ssh will ask the bastion host to resolve the HostName. This allows for local resolution (e.g. private and/or dynamic IP addresses) to be used for the hostname resolution (i.e. a machine may have a name that is only locally resolvable on the bastion). The current code will not forward the name resolution from the bastion host.
  2. there is a hardcoded maximum depth of 10 jump hosts
    3. little to no effort on code style and documentation: this was a quick and dirty feature feasibility test to see if the desired workflow worked correctly
  3. no unit tests have been added

Memet Bilgin added 22 commits December 20, 2023 19:00
this allows different hosts (jump hosts) to have different identity files
specified
the hostKeyCallback makes use of the SSH port and fails if a custom ssh port is
being used by the host
this value was chosen as the lowest RSA available by default on a debian build
running OpenSSH_9.2 and works out of the box for most hosts tested by authority.
Any older systems can specifically set their key algorithms in .ssh/config
@memetb memetb marked this pull request as ready for review February 22, 2024 16:13
@memetb memetb changed the title WIP: expanded ssh_config parameters for qemu+ssh uri option expanded ssh_config parameters for qemu+ssh uri option Feb 22, 2024
@memetb memetb mentioned this pull request Feb 22, 2024
libvirt/uri/ssh.go Outdated Show resolved Hide resolved
libvirt/uri/ssh.go Outdated Show resolved Hide resolved
bastion, err = u.dialHost(proxy, sshcfg, depth + 1)
if err != nil {
return nil, fmt.Errorf("failed to connect to bastion host '%v': %w", proxy, err)
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the err != nil, it is silently ignored instead of returning. My suggestion: if err != nil, return, then only condition on proxy being non empty to execute the recursion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear, can you rephrase please? Did you mean "if the error == nil" ?

Are you perhaps saying there can be a condition in which both bastion and err are nil?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is that proxy, err = sshcfg.Get(target, "ProxyJump") can return error, and in that case you would continue with the flow, instead of returning the function. Yes you would skip dialing the proxy (assuming it returned empty string in addition to the error), but the flow would continue anyway.

Wouldn't it make more sense to return early if proxy, err = sshcfg.Get(target, "ProxyJump") returns an error. And then you execute the code that dials the proxy only testing for proxy != "" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProxyJump can be absent, in which case Get() (as opposed to GetStrict())returns empty string as per sshconfig docs. This isn't an error per-se as it simply means there's no proxy. This covers case 1 of the conditional on line 250, which is err == nil and proxy == "". In this case, the flow should proceed without proxy.

The second case is if the sshconfig file had a parse error (i.e. err != nil) and we don't care about proxy. In this case, it means that the sshconfig file couldn't be parsed. Expected behaviour in those circumstances (e.g. when using ssh from cli) is to simply honor the command itself and ignore any ssh_config directives altogether, which in this case corresponds to a flow-through without Proxy.

Wouldn't it make more sense to return early if proxy, err = sshcfg.Get(target, "ProxyJump") returns an error. And then you execute the code that dials the proxy only testing for proxy != "" ?

I think there's a case to be made that (maybe) the very first successful call to sshcfg.Get() guarantees that every subsequent call is successful. However, I'd not feel comfortable breaking the abstraction and depending on this behaviour: it would make the code very convoluted and introduce race conditions (i.e. if ssh_config gets touched during this setup).

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmacvicar see above for my thoughts on this codepath.

libvirt/uri/ssh.go Outdated Show resolved Hide resolved
libvirt/uri/ssh.go Outdated Show resolved Hide resolved
Copy link
Owner

@dmacvicar dmacvicar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am all for merging this. It needs some quality changes and solving the conflicts

@memetb
Copy link
Contributor Author

memetb commented Sep 14, 2024

@dmacvicar made the changes you requested, 2 outstanding question. Let me know when you get a chance.

Thanks.

bastion, err = u.dialHost(proxy, sshcfg, depth + 1)
if err != nil {
return nil, fmt.Errorf("failed to connect to bastion host '%v': %w", proxy, err)
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is that proxy, err = sshcfg.Get(target, "ProxyJump") can return error, and in that case you would continue with the flow, instead of returning the function. Yes you would skip dialing the proxy (assuming it returned empty string in addition to the error), but the flow would continue anyway.

Wouldn't it make more sense to return early if proxy, err = sshcfg.Get(target, "ProxyJump") returns an error. And then you execute the code that dials the proxy only testing for proxy != "" ?

@dmacvicar dmacvicar merged commit 26b5656 into dmacvicar:main Sep 15, 2024
4 checks passed
@dmacvicar
Copy link
Owner

Thanks for your contribution @memetb , I will try to wrap it up in a release soon.

@dmacvicar dmacvicar added the Important (Wanted) Feature or contribution desired to be had and merged label Sep 15, 2024
@memetb
Copy link
Contributor Author

memetb commented Sep 16, 2024

You're welcome. Thanks for the very useful library.

@memetb memetb deleted the ssh-config-parameters branch September 16, 2024 09:59
@memetb memetb restored the ssh-config-parameters branch September 16, 2024 09:59
@dmacvicar
Copy link
Owner

Do you see anything worth documenting here?

dmacvicar pushed a commit to jimnydev/terraform-provider-libvirt that referenced this pull request Sep 28, 2024
* bump ssh_config to stable version which contains GetAll call
* refactor dialSSH and break out dialHost as support function
* move authentication parsing to per host part of loop

this allows different hosts (jump hosts) to have different identity files
specified

* implement per Host identity file lookup
* fix tilde (~) based home directory notation for convenience
* updated go.sum
* cleanup log outputs
* remove unnecessary local variable
* make use of net package URI building to support correct ipv6

as per commit from MaxMatti:
dmacvicar@1152bdd

* correctly use host:port format when dialing bastion host
* put quotes around target in case it is empty
* if the hostname override isn't present, simply use target name
* add log output for port override
* add default host key algorithm
* move port configuration earlier so that hostkey callback works right
the hostKeyCallback makes use of the SSH port and fails if a custom ssh port is
being used by the host

* cleanup log output, add error handling for dial host
* add support for sshconfig based known hosts file behaviour
* integrate HostKeyAlgorithms ssh_config option
* move dial host impl so that bastion hosts have same features
* add comments
* use a more modern default host key

this value was chosen as the lowest RSA available by default on a debian build
running OpenSSH_9.2 and works out of the box for most hosts tested by authority.
Any older systems can specifically set their key algorithms in .ssh/config

* update auth method parse to allow for multiple private ssh keys
* use a list of hostKeyAlgorithms instead of just one default
* use camelCase to match go coding styles
* use join instead of replace for a more predictable outcome

replace could have resulted in weird behaviour such as "some~path" becoming
incorrectly mangled

* remove log.Fatal and let the upper layer deal with the logging
* change magic number to constant
* code formatting to match go coding style
* add missing import for filepath module (0da4763)
* lint fixes
@mhtr
Copy link

mhtr commented Oct 22, 2024

Unfortunately it ignores the settings in ~/.ssh/config that concern the Port directive. For the bastion node and for the host a custom port is specified, but the connection still goes to port 22.

Host <bastion_ip>
  Port 12345
  User username

Host <libvirt_host>
 Port 12345
 User username
 ProxyJump <bastion_ip>

Error:
Error: failed to connect: failed to connect to bastion host '<bastion_ip>': failed to connect to remote host '<bastion_ip>': dial tcp <bastion_ip>:22: connect: connection refused

If I don't use bastion and specify port in uri like this:

provider "libvirt" {
    uri = "qemu+ssh://username@<libvirt_host>:12345/system"
}

I get an error:
Error: failed to connect: failed to connect to remote host '<libvirt_host>:12345': dial tcp: lookup <libvirt_host>:12345: no such host

@memetb
Copy link
Contributor Author

memetb commented Oct 22, 2024

@mhtr please open a new issue, we can look at it over there.


@dmacvicar I'm thinking of your previous comment about the dispersed nature of the ssh config object. The original motivation of my PR was to enable bastion hosts as they are critical in modern infra, however I aimed to change nothing in the structure of the code and as a result, most of the code simply got moved (not rewritten) for the purpose of disentangling dialSSH and dialHost in 19893fc.

What I'm seeing is that ConnectionURI as an object is probably where this code should lie. Incidentally, this current bug is here in the call to u.Port(), which is incorrect because connectionURI simply inherits the stock URL.Port() functionality.

Further, the unit test for connectionURI seems to me the ideal location to put a battery of unit tests as it relates to ssh_config.

I can fix the u.Port() for now, but if I'm going to do a refactor of connection_uri, I will need some joint review to make sure it's a sensible path forward (but given the note on the project that refactors aren't particularly welcome, I'm not sure we want to take this on). In particular, I think the unit tests should be substantially beefed up, and my personal preference would be to come to a consensus on the unit test itself first: it should be comprehensive in that it should test garbage files, missing files, edge conditions with mixed sources of data...

@mhtr
Copy link

mhtr commented Oct 23, 2024

@memetb
1116

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Important (Wanted) Feature or contribution desired to be had and merged
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants