Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ssh Bundle Laravel Package

spaceheadlabs/ssh-bundle

A Symfony bundle that integrates SSH connectivity into your application, providing services for running remote commands and managing SSH sessions. It wraps SSH client functionality in configurable bundle services, making remote server operations easier to inject and use within Symfony projects.

View on GitHub
Deep Wiki
Context7

Getting Started

The first thing to understand is that this package is a Symfony Bundle, not a Laravel package. It ships a Bundle class plus a DI Extension, which Laravel's container has no concept of. You cannot composer require it into a Laravel app and register it in config/app.php the way you would a normal package — the container wiring expects Symfony's kernel boot sequence. So "getting started" here means reading source before writing anything, not wiring a dependency.

Where to look first, in order:

  1. composer.json — the PHP constraint, Symfony version range, and which transport library it wraps (phpseclib/phpseclib, ext-ssh2, or a shell-out to ssh). This file alone usually determines whether the package is even worth reading further. A shell-out would be disqualifying for most production use.
  2. The DependencyInjection/ directory — the Configuration class is the closest thing to a config/ssh.php schema you'll find. Copy that shape (named connections, host, port, user, auth method) rather than the mechanism.
  3. The Service/ or Client/ directory — the classes actually doing the work. These are the only artifacts with any reusable value.
  4. The Bundle class and Extension — skim to confirm nothing load-bearing (event listeners, compiler passes) lives there. It usually doesn't.

First realistic use case: extract the concrete client class (or two) that perform connect, execute, upload, download, disconnect. Copy them verbatim into app/Services/Ssh/. Then define your own narrow contract before touching them:

// app/Contracts/SshClient.php
interface SshClient {
    public function execute(string $command, int $timeout = 30): SshResult;
    public function upload(string $local, string $remote): bool;
    public function download(string $remote, string $local): bool;
    public function disconnect(): void;
}

Bind that in a Laravel service provider against an implementation built on phpseclib/phpseclib directly. The bundle is a reference implementation, never a runtime dependency. If the extraction turns out to be 200 lines — and it likely will — you delete the extraction and keep the phpseclib implementation, which is the outcome you should be aiming for from step one.

Do not put this on a critical path. Budget half a day to read the source and check the license, then one to two days to spike the phpseclib implementation. Only if the bundle clears both and saves meaningful work do you copy anything.

Implementation Patterns

Config-driven connection registry. The bundle's one genuinely useful idea is declarative named connections rather than credentials scattered through call sites. In Laravel this becomes a flat config file — Symfony's tree-based Configuration class does not carry over, so flatten it:

// config/ssh.php
return [
    'default' => 'production',
    'connections' => [
        'production' => [
            'host'     => env('SSH_HOST'),
            'port'     => 22,
            'username' => env('SSH_USER'),
            'auth'     => 'key',
            'key'      => env('SSH_KEY_PATH'),           // path, never contents
            'passphrase' => env('SSH_KEY_PASSPHRASE'),
            'timeout'  => 30,
            'known_hosts' => storage_path('app/ssh/known_hosts'),
        ],
    ],
];

Connections are stateful — open per job, not per call. Unlike an HTTP client, an SSH connection is a persistent TCP session with a remote sshd that enforces its own MaxSessions and MaxStartups. The pattern that works is: resolve one client per unit of work (one queued job, one command), run the operations, disconnect explicitly in a finally. Do not open a connection inside a loop over hosts without bounding concurrency.

Command execution wrapper returning a result object. The bundle's execute-and-capture-output shape is worth preserving, but return a typed result rather than a string. Exit codes matter and the bundle's likely string-only return loses them:

final class SshResult {
    public function __construct(
        public readonly string $command,
        public readonly string $stdout,
        public readonly string $stderr,
        public readonly int $exitCode,
    ) {}
    public function successful(): bool { return $this->exitCode === 0; }
    public function throw(): self { /* if !successful, throw with stderr */ }
}

Queue jobs for anything non-trivial. Remote command execution is blocking I/O with a remote dependency; run it in a queued job with $timeout and $tries set, and a job-specific timeout on the connection. This is the single most important integration decision and it has nothing to do with the package.

final class RunRemoteDeploy implements ShouldQueue {
    public int $timeout = 120;
    public int $tries = 1;

    public function handle(SshClient $ssh): void {
        $ssh->execute('cd /srv/app && git pull && ./deploy.sh', timeout: 90)->throw();
    }
}

Multi-host fan-out via Bus::batch. When the bundle's "configurable connection profiles" feature tempts you into iterating hosts in a loop, use Laravel's batch instead. Each host gets its own job, its own connection, its own timeout, and failures are isolated and observable.

File transfer: prefer SFTP over heredoc/scp shell-outs. If the bundle implements uploads by piping scp through a command string, discard that path. phpseclib's SFTP subsystem is atomic-friendly and gives you real error handling. For anything larger than a few MB, stream with callbacks rather than loading into memory.

Integration tips. Publish an ADR recording why the bundle was not adopted — future maintainers will otherwise find it again and re-evaluate from scratch. Keep the extracted class behind your SshClient interface so swapping to spatie/ssh (for simple command execution) or dropping to raw phpseclib is a one-line binding change.

Gotchas and Tips

Framework mismatch is the headline gotcha, not a footnote. A Bundle assumes Symfony's DI extension, configuration tree, and kernel lifecycle. In Laravel you're performing a manual source-level integration, which converts "add a dependency" into "own a fork." Name this explicitly before scoping work; it is routinely discovered mid-sprint.

Zero stars, zero dependents, no maintenance signal. Nobody has run this in production and reported back. There is no issue triage, no CVE monitoring, no upstream applying pressure to keep pace with PHP or Symfony releases. For a credential-handling dependency this is the worst possible place to be an early adopter. Treat it as sample code.

Verify host-key verification before anything else. Thin SSH wrappers routinely disable StrictHostKeyChecking for convenience, and it is often configurable in a way an operator can silently switch off. Check the default, then pin a known_hosts file explicitly regardless. A MITM surface is not a configuration preference.

Ask what it wraps. phpseclib is fine; ext-ssh2 is acceptable but a deployment prerequisite; shelling out to the ssh binary is disqualifying for most multi-tenant or containerized setups. This single fact changes the integration effort and the security posture.

Blocking I/O kills PHP-FPM workers. SSH with no timeout stalls a worker indefinitely; under load that cascades into 502s that look like a web-tier problem. Enforce a hard timeout at your adapter layer, not at whatever the package default happens to be, and set $timeout on the queue job as a second line of defence.

Credential leakage through exception traces. Connection config dumped into a stack trace or Laravel's debug page is a real risk. Mark credential properties #[\SensitiveParameter] or redact in render(). Audit this before any non-local environment.

Pooled connections and long-lived processes. If the bundle pools (unlikely given its size, but check), a pool that assumes one-request-per-process will either leak or cross-contaminate credentials under Octane or a long-running queue worker. Assume no pooling and open per job until proven otherwise.

Stale connections after a host reboot. Sessions do not survive the remote sshd restarting. You need retry-once semantics on handshake failure, and a health check before reusing anything cached.

Key rotation has no observed mechanism. Assume it is manual and write the runbook now, while the surface is small.

Symlink/path issues with keys. Resolve key paths with base_path() or storage_path() rather than relative paths — queue workers and schedulers frequently have a different CWD than you expect, and the resulting auth failure is indistinguishable from a wrong key.

Extension points worth using instead of this package. phpseclib/phpseclib for the transport, spatie/ssh for simple one-shot command execution where you don't need SFTP, laravel/envoy if what you actually want is task-runner semantics rather than an SSH client. For orchestration at scale, the honest answer is Ansible or a hosted deploy platform — do not build an in-house SSH layer as a differentiator.

Ramp-up cost is organizational, not technical. A PHP developer comfortable with phpseclib is productive in a day. The real cost is that whoever inherits this owns an unaudited SSH client in a security-sensitive path — assign a named owner before the first production deploy, not after.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json