Provider SSH
Overview
The provider-ssh is a Crossplane provider that reconciles external resources reachable over SSH. It executes user-supplied scripts to converge a remote system to a desired state, exposes observed state from checks, and supports idempotency and drift detection.
Kinds
ProviderConfig: to authenticate/reach the target(s)
SSHTask: A unit of desired work on a host
Installation
You can run the provider-ssh locally or install it from an xpkg file. To install the provider use:
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-ssh
spec:
package: docker.io/etesami/provider-ssh:latest
ProviderConfig
To begin, you'll need to create a ProviderConfig and a Secret.
To initiate a connection to the remote host, either a password or privateKey is required.
The privateKey should be provided as a single-line, base64-encoded string.
To generate the base64 version of a private key, use the following command:
# In Linux
cat .ssh/id_rsa | base64 -w0
The knownHosts file is required to verify the identity of the server.
You can generate and verify it using the command below:
ssh-keyscan <HOST-REMOTE-IP>
Next, construct the ProviderConfig and Secret as shown below:
apiVersion: v1
kind: Secret
metadata:
namespace: crossplane-system
name: providerssh-secret
type: Opaque
stringData:
config: |
{
"username": "ubuntu",
"password": "password",
"privateKey": "5XUUNPV2tSd0ptTFp...wbTNFKzhqMkYzdXc5ClNRZ09QO",
"hostIP": "10.29.30.5",
"hostPort": "22",
"knownHosts": "10.29.30.5 ecdsa-sha2-nistp256 AAAAE2VjZHN...UpvT57WP45MDBAV4CxQ="
}
---
apiVersion: ssh.crossplane.io/v1alpha1
kind: ProviderConfig
metadata:
name: providerssh-config
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: providerssh-secret
key: config
SSHTask
apiVersion: ssh.crossplane.io/v1alpha1
kind: SSHTask
metadata:
name: configure-nginx
spec:
providerConfigRef:
name: default
managementPolicies: ["*"]
forProvider:
scripts:
# 1) checkScript runs first; must be side-effect free. Exit 0 = desired state.
checkScript:
inline: |
if [ -f /etc/nginx/sites-enabled/default ]; then echo "present"; exit 0; else exit 1; fi
# 2) applyScript converges to desired state; MUST be idempotent.
applyScript:
inline: |
sudo apt-get update -y
sudo apt-get install -y nginx
echo "hello" | sudo tee /var/www/html/index.html >/dev/null
# 3) diffScript (optional) outputs machine-readable drift info (e.g., JSON).
diffScript:
inline: |
test -f /var/www/html/index.html || echo '{"file":"missing"}'
# exit 0 regardless; controller treats non-empty stdout as drift details
# 4) cleanupScript runs on Delete.
cleanupScript:
inline: |
sudo apt-get purge -y nginx || true
sudo rm -f /var/www/html/index.html || true
# Execution environment & safety
execution:
sudo: true
shell: /bin/bash -euo pipefail
timeoutSeconds: 600
maxAttempts: 2 # per reconcile
# env is map of key, values where all instance of key is replaced
# by value before executing the script on the remove device
env:
INLINE_VAR: FOO
artifactPolicy:
capture: stdout # stdout | stderr | both | none
status:
conditions:
- type: Ready
status: "True"
reason: UpToDate
lastTransitionTime: "2025-08-18T14:02:31Z"
- type: Synced
status: "True"
reason: ObserveSucceeded
lastTransitionTime: "2025-08-18T14:02:31Z"
atProvider:
endpoint:
host: "10.0.0.12"
port: 22
username: "ubuntu"
lastCheckTime: "2025-08-18T14:02:29Z"
lastRun:
time: "2025-08-18T14:02:29Z"
exitCode: 0
retryCount: 0
artifacts:
stdout: ""
stderr: ""
Development
make build
# The image is stored somewhere like
pkg=provider-ssh-v0.0.0-21.gb69de81.xpkg
VERSION=v2.0.0-rc6 && \
DIR=/home/ubuntu/provider-ssh/_output/xpkg/linux_amd64 && \
crossplane xpkg push -f $DIR/$pkg index.docker.io/etesami/provider-ssh:$VERSION && \
crossplane xpkg push -f $DIR/$pkg index.docker.io/etesami/provider-ssh:latest