Rex-LibSSH

 view release on metacpan or  search on metacpan

.claude/agents/rex-libssh-test-writer.md  view on Meta::CPAN

`AllowUsers` the current user, forked child with stdio to `/dev/null`, polled for up to
5s, `SIGTERM`+`waitpid` in `DESTROY`. Reuse it; do not add a second sshd bootstrap.

```perl
use lib 't/lib';
use TestSSHD;
my $srv = TestSSHD->start;
plan skip_all => 'sshd or ssh-keygen not available' unless $srv;

set connection => 'LibSSH';
Rex::Config->set_private_key( $srv->client_key );
Rex::Config->set_public_key( $srv->client_key . '.pub' );
Rex::connect( server => $srv->host, port => $srv->port,
              user => scalar getpwuid($<), private_key => $srv->client_key,
              public_key => $srv->client_key . '.pub', auth_type => 'key' );
# ... assertions ...
Rex::pop_connection();
```

Note `CORE::open` / `CORE::close` in the existing integration test: `Rex::Commands::Fs`
exports `open`-shaped names into the file, so local filehandle work must be
`CORE::`-qualified. Same for `stat` — bare `stat` there is Rex's remote one, which is
the point.

.claude/skills/rex/SKILL.md  view on Meta::CPAN

set connection => 'LibSSH';

# All Rex file operations now work without SFTP:
file '/etc/hostname', content => "myhost\n";
delete_lines_matching '/etc/fstab', matching => qr/\sswap\s/;
host_entry 'myhost.internal', ip => '127.0.1.1', aliases => ['myhost'];
```

Authentication:
```perl
Rex::Config->set_private_key('/root/.ssh/id_ed25519');
Rex::Config->set_public_key('/root/.ssh/id_ed25519.pub');
```

Host key checking is disabled by default (`strict_hostkeycheck => 0`).

## Getty's Rex distributions (CPAN)

### Rex::GPU (`Rex-GPU`)

```perl

CLAUDE.md  view on Meta::CPAN

- `Rex::Interface::Exec::LibSSH` — command execution via SSH exec channels
- `Rex::Interface::Fs::LibSSH` — `is_file`, `stat`, `ls`, etc. via exec
- `Rex::Interface::File::LibSSH` — file upload/download via `cat`/heredoc exec

## Key Details

- Host key verified against `known_hosts` by default; opt out per-connection
  with `strict_hostkeycheck => 0` or Rexfile-wide with
  `-feature => ['disable_strict_host_key_checking']` (CWE-322 fix, requires
  Net::LibSSH >= 0.004)
- Public key auth via `Rex::Config->set_private_key` / `set_public_key`
- No SFTP subsystem needed on remote host
- Used by Rex::Rancher and Rex::GPU for Hetzner dedicated server deployments

## Dependencies

- `Net::LibSSH` (XS binding for libssh)
- `Alien::libssh` (provides the libssh C library, via Net::LibSSH)
- `Rex` (framework)

Consumed downstream by `Rex::GPU` and `Rex::Rancher`, which both

lib/Rex/Interface/Connection/LibSSH.pm  view on Meta::CPAN

}

sub connect {
    my ( $self, %opt ) = @_;

    my $server   = $opt{server};
    my $port     = $opt{port}     || Rex::Config->get_port( server => $server )    || 22;
    my $timeout  = $opt{timeout}  || Rex::Config->get_timeout( server => $server ) || 10;
    my $user     = $opt{user};
    my $password = $opt{password};
    my $privkey  = $opt{private_key};
    my $auth     = $opt{auth_type} // 'key';

    $self->{server}        = $server;
    $self->{is_sudo}       = $opt{sudo};
    $self->{__auth_info__} = \%opt;

    ( $server, $port ) = Rex::Helper::IP::get_server_and_port( $server, $port );

    # Host key verification: a per-connect option wins, otherwise the same
    # openssh_opt knobs the OpenSSH backend honours -- StrictHostKeyChecking

lib/Rex/LibSSH.pm  view on Meta::CPAN

Rex's interface dispatch will automatically load
L<Rex::Interface::Connection::LibSSH>,
L<Rex::Interface::Exec::LibSSH>,
L<Rex::Interface::Fs::LibSSH>, and
L<Rex::Interface::File::LibSSH>.

=head2 Authentication

Supports public key authentication:

  Rex::Config->set_private_key('/home/user/.ssh/id_ed25519');
  Rex::Config->set_public_key('/home/user/.ssh/id_ed25519.pub');

Or pass keys directly to C<Rex::connect>:

  Rex::connect(
      server      => '10.0.0.1',
      user        => 'root',
      private_key => '/path/to/key',
      public_key  => '/path/to/key.pub',
      auth_type   => 'key',
  );

=head2 Host key verification

The server's host key is verified against C<known_hosts> by default, exactly
like an interactive C<ssh> client but without the prompt: an unknown or
changed key makes the connection fail before any authentication is
attempted. Requires L<Net::LibSSH> 0.004 or later — earlier versions of

t/01-rex-integration.t  view on Meta::CPAN

use Rex -feature => ['1.4'];
use Rex::Group::Entry::Server;
use Rex::Commands::Run;
use Rex::Commands::Fs;
use Rex::Commands::File;
use Rex::Config;

set connection => 'LibSSH';

Rex::Config->set_user( scalar getpwuid($<) );
Rex::Config->set_private_key( $srv->client_key );
Rex::Config->set_public_key( $srv->client_key . '.pub' );

Rex::connect(
    server      => $srv->host,
    port        => $srv->port,
    user        => scalar( getpwuid($<) ),
    private_key => $srv->client_key,
    public_key  => $srv->client_key . '.pub',
    auth_type   => 'key',
    knownhosts  => $srv->known_hosts,
);

# --- run ---
my $out = run 'echo hello';
chomp $out;
is $out, 'hello', 'run echo works';

t/02-exec-signature.t  view on Meta::CPAN


use Rex -feature => ['1.4'];
use Rex::Group::Entry::Server;
use Rex::Commands::Run;
use Rex::Commands::Fs;
use Rex::Config;

set connection => 'LibSSH';

Rex::Config->set_user( scalar getpwuid($<) );
Rex::Config->set_private_key( $srv->client_key );
Rex::Config->set_public_key( $srv->client_key . '.pub' );

Rex::connect(
    server      => $srv->host,
    port        => $srv->port,
    user        => scalar( getpwuid($<) ),
    private_key => $srv->client_key,
    public_key  => $srv->client_key . '.pub',
    auth_type   => 'key',
    knownhosts  => $srv->known_hosts,
);

# ---------------------------------------------------------------------------
# 1. Env hash handover through the 4-arg exec($cmd, $path, $option) signature.
#
# Rex::Commands::Run::run invokes $exec->exec($cmd, $path, $option); the 2-arg
# form on Exec::Base bound $path to $option, so $option ended up undef and the

t/03-path-quoting.t  view on Meta::CPAN


use Rex -feature => ['1.4'];
use Rex::Group::Entry::Server;
use Rex::Commands::Fs;
use Rex::Commands::File;
use Rex::Config;

set connection => 'LibSSH';

Rex::Config->set_user( scalar getpwuid($<) );
Rex::Config->set_private_key( $srv->client_key );
Rex::Config->set_public_key( $srv->client_key . '.pub' );

Rex::connect(
    server      => $srv->host,
    port        => $srv->port,
    user        => scalar( getpwuid($<) ),
    private_key => $srv->client_key,
    public_key  => $srv->client_key . '.pub',
    auth_type   => 'key',
    knownhosts  => $srv->known_hosts,
);

# Use a fresh tempdir on the remote side (the sshd is local, so a
# local /tmp path is visible to the remote user too).
my $dir = tempdir(CLEANUP => 1);

# Local source file for upload(). All "remote file creation" goes

t/04-no-sftp.t  view on Meta::CPAN

use Rex -feature => ['1.4'];
use Rex::Group::Entry::Server;
use Rex::Commands::Run;
use Rex::Commands::Fs;
use Rex::Commands::File;
use Rex::Config;

set connection => 'LibSSH';

Rex::Config->set_user( scalar getpwuid($<) );
Rex::Config->set_private_key( $srv->client_key );
Rex::Config->set_public_key( $srv->client_key . '.pub' );

Rex::connect(
    server      => $srv->host,
    port        => $srv->port,
    user        => scalar( getpwuid($<) ),
    private_key => $srv->client_key,
    public_key  => $srv->client_key . '.pub',
    auth_type   => 'key',
    knownhosts  => $srv->known_hosts,
);

# Every Fs/File operation must work via SSH exec channels alone. On a host
# without an SFTP subsystem, the OpenSSH/SSH backends crash with
# "Can't call method \"stat\" on an undefined value". LibSSH should sail
# through, and that is the contract this test pins down.

t/05-hostkey.t  view on Meta::CPAN

use Rex -feature => ['1.4'];
use Rex::Group::Entry::Server;
use Rex::Commands::Run;
use Rex::Config;
use Rex::Interface::Connection;
use Rex::TaskList;

set connection => 'LibSSH';

Rex::Config->set_user( scalar getpwuid($<) );
Rex::Config->set_private_key( $srv->client_key );
Rex::Config->set_public_key( $srv->client_key . '.pub' );

sub with_timeout {
    my ( $seconds, $code ) = @_;
    my $result;
    local $@;
    eval {
        local $SIG{ALRM} = sub { die "TIMEOUT\n" };
        alarm($seconds);
        $result = $code->();

t/05-hostkey.t  view on Meta::CPAN

    system( 'ssh-keygen', '-t', 'ed25519', '-N', '', '-f', "$dir/key", '-q' ) == 0
        or die "ssh-keygen failed";
    return "$dir/key.pub";
}

sub base_opts {
    return (
        server      => $srv->host,
        port        => $srv->port,
        user        => scalar getpwuid($<),
        private_key => $srv->client_key,
        public_key  => $srv->client_key . '.pub',
        auth_type   => 'key',
    );
}

# Rex::connect dies before push_connection when is_connected is false, so
# there is nothing to pop in the normal "refused" case below. This is a
# defensive net around that, not the expected path: pop only if a
# connection was actually pushed, i.e. either Rex::connect unexpectedly
# succeeded (which would mean a refusal test regressed) or it died after

t/05-hostkey.t  view on Meta::CPAN


    my $task_output;
    task 'hostkey_task_path_test', sub {
        $task_output = run 'echo task-path-ok';
    };

    my $server = Rex::Group::Entry::Server->new(
        name        => $srv->host,
        port        => $srv->port,
        user        => scalar getpwuid($<),
        private_key => $srv->client_key,
        public_key  => $srv->client_key . '.pub',
        auth_type   => 'key',
    );

    my $task = Rex::TaskList->create()->get_task('hostkey_task_path_test');
    eval { $task->run($server) };
    my $err = $@;
    ok !$err,
        'task path: Rex::Task->run() against an unknown host key succeeds via the openssh_opt opt-out'
        or diag "task run died: $err";



( run in 0.621 second using v1.01-cache-2.11-cpan-036bef1c656 )