Solving Let's Encrypt Challenges with SCP for Limited Web Hosting
Contents
The Problem: Limited Domain Hosting Without API Access
If you’ve ever tried to set up SSL certificates with Let’s Encrypt on a domain hosting provider that doesn’t offer API access, you know the struggle. My situation was particularly challenging:
- My domain is hosted with a provider that doesn’t offer any API for automated certificate management
- My web space is limited, making it difficult to run a full ACME client directly on the server
- I needed to automate the certificate renewal process to avoid manual intervention every 90 days
The standard approach for Let’s Encrypt validation is to place a specific challenge file in the .well-known/acme-challenge/
directory of your website. But how do you do this when you can’t run software directly on your hosting provider?
Enter Lego-SCP-Solver
To solve this problem, I created a Go-based tool that leverages the excellent lego ACME client library with a custom challenge solver that uses SCP (Secure Copy Protocol) to upload the challenge files to my web server.
The tool works by:
- Connecting to your web server via SSH
- Creating the required
.well-known/acme-challenge/
directory - Uploading the challenge token file via SCP
- Verifying the file is accessible via HTTP
- Cleaning up after the challenge is complete
This approach allows me to run the certificate issuance process from my local machine or a CI/CD pipeline, without needing to install anything on the web server itself.
How It Works
The core of the solution is a custom HTTP-01 challenge provider that implements the lego challenge interface. Here’s a simplified version of how it works:
// Present implements the challenge.Provider interface
func (s *SCPSolver) Present(domain, token, keyAuth string) error {
// Connect to SSH
if err := s.connect(); err != nil {
return fmt.Errorf("SSH connection failed: %w", err)
}
defer s.sshClient.Close()
// Create remote directory
remotePath := s.webrootPath + "/.well-known/acme-challenge"
if err := s.createRemoteDir(remotePath); err != nil {
return fmt.Errorf("failed to create remote directory: %w", err)
}
// Upload challenge file
remoteFile := remotePath + "/" + token
if err := s.uploadFile(remoteFile, keyAuth); err != nil {
return fmt.Errorf("failed to upload challenge file: %w", err)
}
// Set proper permissions for web server access
if err := s.setPermissions(remotePath, remoteFile); err != nil {
log.Printf("Warning: Failed to set permissions: %v", err)
}
return nil
}
When Let’s Encrypt needs to validate domain ownership, this code:
- Establishes an SSH connection to the web server
- Creates the challenge directory if it doesn’t exist
- Uploads the challenge token with the correct content
- Sets appropriate permissions so the web server can serve the file
After validation, a similar cleanup function removes the challenge file.
Setting Up and Using the Tool
Using the tool is straightforward. You can either set environment variables:
export LEGO_SCP_HOST="your-server.com"
export LEGO_SCP_USER="your-username"
export LEGO_SCP_KEY_PATH="/path/to/your/ssh/private/key"
export LEGO_SCP_WEBROOT_PATH="/var/www/html"
export LEGO_SCP_EMAIL="your-email@example.com"
export LEGO_SCP_DOMAINS="example.com,www.example.com"
export LEGO_SCP_ACCOUNT_KEY="/path/to/account.key"
export LEGO_SCP_CERT_PATH="/path/to/certificates"
Or use command-line flags:
lego-scp-solver -e your-email@example.com -d example.com,www.example.com \
--scp-host your-server.com --scp-user username --scp-key ~/.ssh/id_rsa \
--scp-webroot /var/www/html --cert-path ./certificates
Automating with GitHub Actions
To fully automate the certificate renewal process, I set up a GitHub Actions workflow that runs the tool on a schedule:
name: Renew SSL Certificates
on:
schedule:
- cron: "0 0 1 * *" # Run on the 1st of every month
workflow_dispatch: # Allow manual triggering
jobs:
renew:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.21"
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- name: Run certificate renewal
env:
LEGO_SCP_HOST: ${{ secrets.SCP_HOST }}
LEGO_SCP_USER: ${{ secrets.SCP_USER }}
LEGO_SCP_KEY_PATH: ~/.ssh/id_rsa
LEGO_SCP_WEBROOT_PATH: ${{ secrets.WEBROOT_PATH }}
LEGO_SCP_EMAIL: ${{ secrets.ACME_EMAIL }}
LEGO_SCP_DOMAINS: ${{ secrets.DOMAINS }}
LEGO_SCP_ACCOUNT_KEY: account.key
run: |
go run .
- name: Upload certificates
uses: actions/upload-artifact@v3
with:
name: certificates
path: ./*.crt
This workflow securely stores all sensitive information in GitHub Secrets and runs the certificate renewal process automatically.
Benefits of This Approach
- No server-side installation required: Works with any hosting provider that offers SSH/SCP access
- Fully automated: Set it and forget it - certificates renew automatically
- Secure: Uses SSH key authentication for secure file transfers
- Flexible: Works with multiple domains and subdomains
- Lightweight: Minimal dependencies and resource usage
The Lego ACME Library: A Powerful Foundation
Lego is a Let’s Encrypt client and ACME library written in Go. It provides a complete solution for obtaining, renewing, and revoking SSL certificates from Let’s Encrypt and other ACME-compatible certificate authorities.
What makes Lego particularly powerful is its extensibility. It supports multiple challenge types (HTTP-01, DNS-01, TLS-ALPN-01) and comes with built-in providers for many popular DNS services and hosting platforms. However, its true strength lies in its ability to be extended with custom challenge solvers.
The Easier Alternative: Using Supported DNS Providers
While my SCP solution works well for my specific constraints, I should mention that there’s an easier path if you have the flexibility to choose your domain registrar or DNS provider. Lego has built-in support for over 150 DNS providers, making certificate issuance much simpler if you use one of these services.
With DNS-01 validation through a supported provider, you can:
- Issue wildcard certificates (which isn’t possible with HTTP validation)
- Validate domains without exposing your web server to the internet
- Automate the entire process without custom code
- Issue certificates even when port 80 is blocked
When using DNS-01 validation, Lego creates a TXT record at _acme-challenge.yourdomain.com
that Let’s Encrypt verifies. You can check this record yourself using nslookup:
$ nslookup -q=TXT _acme-challenge.yourdomain.com 8.8.8.8
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
_acme-challenge.yourdomain.com text = "IyxgKAO2vD-GRuMQgJfDKI8zcJRZwjTkYOv_xgAQmq4"
Authoritative answers can be found from:
yourdomain.com nameserver = ns1.example-dns.com.
yourdomain.com nameserver = ns2.example-dns.com.
This TXT record contains the validation token that proves you control the domain.
If you’re starting a new project or can migrate your DNS, consider using one of these supported providers:
- AWS Route 53
- Cloudflare
- DigitalOcean
- Google Cloud DNS
- Azure DNS
- OVH
- Namecheap
- And many more
With these providers, certificate issuance is as simple as:
lego --email you@example.com --domains example.com --dns provider-name --dns.resolvers 1.1.1.1 run
This approach eliminates the need for custom challenge solvers like my SCP solution. However, if you’re stuck with a hosting provider that isn’t on the list (as I was), then a custom solution like lego-scp-solver becomes necessary.
Key Features of Lego
- Complete ACME protocol implementation
- Support for multiple challenge types
- Built-in providers for many DNS services
- Certificate management (obtain, renew, revoke)
- Library mode for integration into other Go applications
- Command-line tool for standalone use
Writing a Custom Challenge Solver
One of the most powerful aspects of Lego is the ability to write custom challenge solvers. The official documentation provides a clear guide on how to do this.
To create a custom challenge solver, you need to implement the appropriate provider interface. For HTTP-01 challenges (like in my SCP solver), you implement the challenge.Provider
interface, which requires two methods:
type Provider interface {
Present(domain, token, keyAuth string) error
CleanUp(domain, token, keyAuth string) error
}
The Present
method is called when the challenge needs to be set up, and CleanUp
is called after the challenge is complete. This simple interface makes it easy to implement custom solutions for any hosting environment.
Here’s a basic template for creating a custom HTTP-01 challenge solver:
type MyCustomSolver struct {
// Your configuration fields here
}
func NewMyCustomSolver(config YourConfig) *MyCustomSolver {
return &MyCustomSolver{
// Initialize with your config
}
}
// Present implements the challenge.Provider interface
func (s *MyCustomSolver) Present(domain, token, keyAuth string) error {
// 1. Create the challenge directory if needed
// 2. Create the challenge file with keyAuth as content
// 3. Make it accessible via HTTP
return nil
}
// CleanUp implements the challenge.Provider interface
func (s *MyCustomSolver) CleanUp(domain, token, keyAuth string) error {
// Remove the challenge file
return nil
}
// Then in your main code:
func main() {
// ... initialize lego client
mySolver := NewMyCustomSolver(config)
client.Challenge.SetHTTP01Provider(mySolver)
// ... proceed with certificate request
}
This flexibility allowed me to create the SCP-based solver for my specific hosting environment constraints.
Conclusion
If you’re stuck with a web hosting provider that doesn’t offer Let’s Encrypt integration or API access, this tool might be just what you need. It bridges the gap between modern automated certificate management and traditional web hosting environments.
The power of Lego’s extensible architecture makes it possible to solve unique challenges like limited web hosting environments. By implementing a custom challenge solver, I was able to automate certificate management even with the constraints of my hosting provider.
The full source code is available on GitHub under the MIT license. Feel free to use it, fork it, or contribute to make it even better!
Remember, everyone deserves free, automated SSL certificates - even if your hosting provider hasn’t caught up with the times yet.
Author SlashGordon
LastMod 2025-06-18