acmesh-official / acme.sh

A pure Unix shell script implementing ACME client protocol
https://acme.sh
GNU General Public License v3.0
37.58k stars 4.83k forks source link

Add deploy hook support to deploy certs to nginx/apache/exim4/dovecot/cpanel/WebFaction etc. #285

Open Neilpang opened 7 years ago

Neilpang commented 7 years ago

It turns out that so many users don't know how to deloy the certs at all.

Just similar like what we did for dns api hooks, I'd like to add hooks to deploy certs to a number of servers, such as: nginx/apache/exim4/dovecot/cpanel/WebFaction etc.

It may be used like:

#issue the cert
acme.sh  --issue  -d mydomain.com  -w ...........

#deply the certs
acme.sh  --deploy  -d mydomain.com  --nginx

# or
acme.sh  --deploy  -d mydomain.com  --apache

# or
acme.sh  --deploy  -d mydomain.com  --webfaction

# etc.

It's an early thought, but let's see.

Theliel commented 7 years ago

Hi @Neilpang

Some good news for cpanel. I'd successful deploy my test cert in one domain. cpanel API info is more or less clear.

cpanel API use 3 auth options, but only web tokens or plain user/pass dont required root or WHM access (so in theory, should work with most of all cpanel account). I believe that Web token is useless here (cpanel give you a session token after loggin), so best options should be user/pass. Port 2083 use SSL connection (security measures)

The problem... i believe perl or php dependence is needed. In my case, a simple perl script. Note: All without root access, only SSH, on Godaddy Shared Hosting Linux Cpanel. Encode and UTF is needed only for parsing the output, no needed for deploy. LWP is needed because the header need to include the auth.

My skills with shell scripts are limited, so i dont know if perl/php can be bypasses and use a pure shell, or in the worse case, maybe acme.sh could add perl/php scripts too.

#!/usr/bin/perl
use strict;
use LWP::UserAgent;
use LWP::Protocol::https;
use Encode;
use utf8;
use JSON;
use MIME::Base64;

my $username = "cpanel_username";
my $password = "cpanel_pass";

my $request = "https://127.0.0.1:2083/execute/SSL/install_ssl";

$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;

my $ua = LWP::UserAgent->new();

$ua->default_header(
    'Authorization' => 'Basic ' . MIME::Base64::encode("$username:$password"),
);

my ( $cert, $key );
{
    local $/;
    open ( my $fh, '<', '/path/to/example.com.cer' );
    $cert = <$fh>;
    close $fh;

    open ( $fh, '<', '/path/to/example.com.key );
    $key = <$fh>;
    close $key;
}

my $response = $ua->post($request,
    Content_Type => 'form-data',
    Content => [
        domain => 'example.com',
        cert   => $cert,
        key    => $key,
    ],
);

#print result/output
my $json_printer = JSON->new->pretty->canonical(1);

my $content = JSON::decode_json(Encode::encode_utf8($response->decoded_content));

print Encode::encode_utf8($json_printer->encode($content));

Work perfecly for me.

If acme.sh can add this in some form (in issues and renew), im sure a lot of ppl will be very happy. For example, Godaddy users can have a complete automatic issue+deploy+renew.

If i can help you to the project in someway, please, tell me.

Neilpang commented 7 years ago

@Theliel Thanks for you info. It helps a lot. I read your perl code, there is no problem to re-write it in shell.

I just don't have a cpanel account to test. let me think about it.

Theliel commented 7 years ago

Good to know @Neilpang .

For privacy concerns with my clients, i can't give you access to my account... personally i don't have any problem, but my cpanel account manage a good amount of domain/host accounts.

If I can help you in anyway, test code, or anything... please, tell me, I will be happy to help you in what I can.

Thank again for your work and time.

Neilpang commented 7 years ago

@Theliel I will purchase a web hosting in godaddy to test. Which plan do you use? Deluxe or Ultimate ?

Theliel commented 7 years ago

I have a old Grid Account (not available now, and not use cpanel anyway), deluxe (to update to ultimate this year probably) and plex (some customers insist on using asp.net applications) . Anyway shouldn't affect anyway, both plans use the same cpanel interface/versions.

Maybe (no sure) I could try with another cpanel hosting this week too.

Theliel commented 7 years ago

Recursive and more generic to renew/install all certs in the server issues by Let's Encrypt, is a nightmare the other one... functional, but time consuming after all:

#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use LWP::Protocol::https;
use Encode;
use utf8;
use JSON;
use MIME::Base64;

#cPanel Auth data required
my $username = "cpanel_user";
my $password = "cpanel_pass";

my $request = "https://127.0.0.1:2083/execute/SSL/install_ssl";

$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;

my $ua = LWP::UserAgent->new();

$ua->default_header(
    'Authorization' => 'Basic ' . MIME::Base64::encode("$username:$password"),
);

#Im assuming this script is copied on acme.sh certhome folder, /home/cpanel_user/.acme.sh by default
my $dir = "./";

opendir DIR, $dir or die "cannot open dir $dir: $!";
my @file= grep { -d $_ && $_ =~ /[^.].*[.].*/} readdir DIR;
closedir DIR;

foreach my $file (@file) { 
    print "$file\n";
    my ( $cert, $key );
    {
        local $/;
        open ( my $fh, '<', "$file/$file.cer" );
        $cert = <$fh>;
        close $fh;

        open ( $fh, '<', "$file/$file.key" );
        $key = <$fh>;
        close $key;
    }

    my $response = $ua->post($request,
        Content_Type => 'form-data',
        Content => [
            domain => $file,
            cert   => $cert,
            key    => $key,
        ],
    );

    my $json_printer = JSON->new->pretty->canonical(1);
    my $content = JSON::decode_json(Encode::encode_utf8($response->decoded_content));
    print Encode::encode_utf8($json_printer->encode($content));
}
Neilpang commented 7 years ago

thanks

FernandoMiguel commented 7 years ago

what's the current status of whm/cpanel support?

Neilpang commented 7 years ago

@FernandoMiguel

I was setting a testing cpanel machine, but I need more time.

Theliel commented 7 years ago

@Neilpang,

im seeing that cpanel dont remove expired certs. install_ssl API install/update the certs fine, but expired certs on the server remain forever, so the list and db grow and grow. or you can remove them from cpanel.

Im finishing a new .pl to take this in consideration, using list_certs and delete_cert. The first one to parse expired certs info, the second to remove any expired cert.

Maybe will be more "simple" remove the previous installed cert just before install the new one.

Neilpang commented 7 years ago

@Theliel show me your code.

Theliel commented 7 years ago

is not finished yet, sorry. I post/send you as soon as I have it

Neilpang commented 7 years ago

@Theliel Do you know where I can buy a cheap cpanel host ? I want to buy one to test.

FernandoMiguel commented 7 years ago

@Neilpang let me call in a favor with a friend interested in getting this moving

Neilpang commented 7 years ago

@FernandoMiguel Thank you.

FernandoMiguel commented 7 years ago

@Neilpang give @PJFonseca a few hours till he get home from work (UTC TZ) he will try to provide you with an account for testing

Theliel commented 7 years ago

@Neilpang

My actual pl script.

First, I remove any expired cert on the server (cpanel dont remove them, and when I realized, I had more than a hundred installed certs). list_certs to retrieve all certs and delete_cert to remove the expired one (date comparison: current-time > not-after)

The second part is the same posted time ago, install_ssl with the forged url.

#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use LWP::Protocol::https;
use Encode;
use JSON;
use MIME::Base64;
use Time::HiRes qw(gettimeofday);

#cPanel Access Data
my $username = "myusername";
my $password = "mypassword";

#cPanel API
my $cert_install = "https://127.0.0.1:2083/execute/SSL/install_ssl";
my $cert_list = "https://127.0.0.1:2083/execute/SSL/list_certs";
my $cert_delete = "https://127.0.0.1:2083/execute/SSL/delete_cert";

my $time_seconds;
my $time_micro;

$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;

#HTTP header conf
my $ua = LWP::UserAgent->new();
$ua->default_header(
    'Authorization' => 'Basic ' . MIME::Base64::encode("$username:$password"),
);

#DELETING ONLY EXPIRED CERTS

my $response_list = $ua->get($cert_list,);

#Convert GET response to perl JSON structure
my $content_list = decode_json($response_list->decoded_content);

($time_seconds, $time_micro) = gettimeofday;

#walking through JSON stricture to extract ID and expired time.
foreach my $cert_id ( @{$content_list->{data} }){
    if ($time_seconds > $cert_id->{not_after}) {
        #Delete expired cert
        my $response_delete = $ua->post($cert_delete,
            Content_Type => 'form-data',
            Content => [
            id => $cert_id->{id},
            ],
        );

        print "Deleting " . $cert_id->{friendly_name} . ", expired on " . scalar localtime($cert_id->{not_after}) . "\n";
        }
}

#INSTALLING NEW CERTS

#Work Folder, (where acme.sh store certs)
my $dir = "./";

opendir DIR, $dir or die "cannot open dir $dir: $!";
my @file= grep { -d $_ && $_ =~ /[^.].*[.].*/} readdir DIR;
closedir DIR;

foreach my $file (@file) { 
    print "$file\n";
    my ( $cert, $key );
    {
        local $/;
        open ( my $fh, '<', "$file/$file.cer" );
        $cert = <$fh>;
        close $fh;

        open ( $fh, '<', "$file/$file.key" );
        $key = <$fh>;
        close $key;
    }

    my $response_install = $ua->post($cert_install,
        Content_Type => 'form-data',
        Content => [
            domain => $file,
            cert   => $cert,
            key    => $key,
        ],
    );

    my $json_printer = JSON->new->pretty->canonical(1);
    my $content_install = decode_json($response_install->decoded_content);
    print $json_printer->encode($content_install);
}

Much better now.

Another option could be removed directly all certificates without distinction (expired and not), but a user might have some certificate in use not issued by LetEncrypt. Is not perfect, don't remove old LetEncrypt cert not yet expired (renew happen before expired date), but are removed in the next cycle anyway.

About Cheap cPanel... I don't know, sorry. I hope the friend Fonseca can help you

If i can help you in any other way, please, tell me about it.

codedmind commented 7 years ago

@Neilpang do you still need a cpanel account?

Neilpang commented 7 years ago

@codedmind

Yes, that would help you a lot.

Thanks.

codedmind commented 7 years ago

Give me an email please and i create one for you

Com os melhores cumprimentos, Diogo Serra

2017-04-20 7:57 GMT+01:00 neil notifications@github.com:

@codedmind https://github.com/codedmind

Yes, that would help you a lot.

Thanks.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Neilpang/acme.sh/issues/285#issuecomment-295602286, or mute the thread https://github.com/notifications/unsubscribe-auth/ACxnX_-FqEk7Hrki5w4ZEiMrOeJ6Cqljks5rxwHrgaJpZM4J5Gss .

Neilpang commented 7 years ago

@codedmind

Send to info[A-T]acme.sh

Thanks.

ayavilevich commented 6 years ago

Hi all, I have used this code to successfully deploy the issued cert to godaddy shared cPanel. https://www.travel-forum.2globalnomads.info/viewtopic.php?f=16&t=24&p=30 It is shell code only and uses the UAPI command.

#!/usr/bin/env sh
# Here is the script to deploy the cert to your cpanel using the cpanel API.
# Uses command line uapi. 
# Cpanel username is needed only when run as root (I did not test this).
# Returns 0 when success.
# Written by Santeri Kannisto <santeri.kannisto@2globalnomads.info>
# Public domain, 2017

#export DEPLOY_CPANEL_USER=myusername

########  Public functions #####################

#domain keyfile certfile cafile fullchain

cpanel_deploy() {
  _cdomain="$1"
  _ckey="$2"
  _ccert="$3"
  _cca="$4"
  _cfullchain="$5"

  _debug _cdomain "$_cdomain"
  _debug _ckey "$_ckey"
  _debug _ccert "$_ccert"
  _debug _cca "$_cca"
  _debug _cfullchain "$_cfullchain"

  # read cert and key files and urlencode both
  _certstr=$(cat "$_ccert")
  _keystr=$(cat "$_ckey")
  _cert=$(php -r "echo urlencode(\"$_certstr\");")
  _key=$(php -r "echo urlencode(\"$_keystr\");")

  _debug _cert "$_cert"
  _debug _key "$_key"

  if [ "$(id -u)" = 0 ]; then
    _opt="--user=$DEPLOY_CPANEL_USER"
    _debug _opt "$_opt"
  fi

  _response=$(uapi $_opt SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")

  if [ $? -ne 0 ]; then
    _err "Error in deploying certificate:"
    _err "$_response"
    return 1
  fi

  _debug response "$_response"
  _info "Certificate successfully deployed"
  return 0
}
Theliel commented 6 years ago

Work fine, but if I remember well, you have the problem that you need deploy the cert every time your certs are updated.

And another problem, godaddy/cpanel dont remove old certs. Each time you deploy a certificate, the olds remains stored, which makes the database only grow, and if you have multiple sites you have a problem. The script need to remove the old cert first (uapi SSL delete_cert domain=example.com), then deploy the new one.

ayavilevich commented 6 years ago

@Theliel thanks for your comments.

This deploy module is registered with acme (through acme.sh --deploy -d example.com --deploy-hook cpanel) so I am expecting it to run every time the cert is updated. I have tried the "renew" command with "--force" and it renewed and deployed the new certificate. Do you think I am missing something?

With regard to old certs piling up, I think you are right. However I am not sure if this is normal for cPanel or not. I guess they didn't expect certificates to be replaced this often in their original design. The cPanel web UI only shows the ones that are in use, ignoring the unused ones. But the used ones and the unused ones can be listed with "UAPI SSL list_certs". Further more, if I delete a certificate in the web UI I can still see it in the DB. Perhaps it is not a big issue.

If it is ever an issue then I will use your script periodically to delete expired certs.

Theliel commented 6 years ago

@ayavilevich

Umm, if deploy is applied on renew too, should be a good new.

I don't know general cPanel behavior, but at least specifically with Godaddy, they dont remove older certs. They are not... "assigned" to some domain, are only "installed". You can see them on "Certificates (CRT)", not under Install and Manage. Of course, list_cert show all of them

Anyway, this bash script is much more simple and use uapi directly (i didn't know that we had access to uapi), passing cert/key directly (not searching on specific folder), so he only need a little change to remove (or check and remove) the old certificate before deploy the newer

If I have some time, i will try with auto-renew/deploy and I will try again with a sightly modded script. If work fine, should be sufficient removing (only one time) old certs to clean the DB, and no more manual actions, not for renew, deploy or clean :)

Thank for the point.

Edit: Maybe, a problem can be the use of php. @Neilpang use a pure shell solution, i don't know if he will like / allow to use it for general purpose.

Theliel commented 6 years ago

Hi again:

@ayavilevich you are right, domain config file is modded to add the deploy hook, so renews should call deploy too.

About removing php dependence, was easy, I am sure there is a better way to do it, but for now I have not encountered problems, and on the other hand it is quite faster than using php.

About the problem with old certs, right now, my solution is a little... "ugly". cPanel use friendlyname or ID to identify a installed cert. ID and friendly name is created on installation time (deploying) and you can't retrieve easily once is already installed. The only option I found (used on my pl script) was to parse the JSON response of list_certs to retrieve ID/friendlyname and them calling delete_cert.

this time, parsing complete list_certs without json, searching for domain_xxx to retrieve the complete ID (I don't know in others cPanel hostings). Format used for id is domain_xxx_keyID (need awk, tr and grep)

Maybe @Neilpang know a better way, or of course, another option is to simply ignoring the old installed certs, but it would not be suitable/recommendable on a server with dozens of domains

in my first tests, work fine, but I not had time to test it extensively, and my bash skills are not the best :)

#!/usr/bin/env sh
#Here is the script to deploy the cert to your cpanel account by the cpanel APIs.
#returns 0 means success, otherwise error.

#export DEPLOY_CPANEL_USER=myusername

########  Public functions #####################

#domain keyfile certfile cafile fullchain

cpanel_deploy() {
  _cdomain="$1"
  _ckey="$2"
  _ccert="$3"
  _cca="$4"
  _cfullchain="$5"

  _debug _cdomain "$_cdomain"
  _debug _ckey "$_ckey"
  _debug _ccert "$_ccert"
  _debug _cca "$_cca"
  _debug _cfullchain "$_cfullchain"

  # read cert and key files and urlencode (without php) both
  _certstr=$(cat "$_ccert")
  _keystr=$(cat "$_ckey")
  _cert=$(urlencode "$_certstr")
  _key=$(urlencode "$_keystr")

  _debug _cert "$_cert"
  _debug _key "$_key"

  if [ "$(id -u)" = 0 ]; then
    _opt="--user=$DEPLOY_CPANEL_USER"
    _debug _opt "$_opt"
  fi

  # remove old cert from cpanel
  _id=$(echo $_cdomain | tr "." "_")
  _id=$(uapi $_opt SSL list_certs | grep $_id | awk '{ print $2 }')
  _response_del=$(uapi $_opt SSL delete_cert id=$_id)

  # deploying certs
  _response=$(uapi $_opt SSL install_ssl domain="$_cdomain" cert="$_cert" key="$_key")

  if [ $? -ne 0 ]; then
    _err "Error in deploying certificate:"
    _err "$_response"
    return 1
  fi

  _debug response "$_response_del"
  _debug response "$_response"
  _info "Certificate successfully deployed"
  return 0
}

  #urlencode function to remove php dependence
urlencode() {
    local LANG=C
    local length="${#1}"
    for (( i = 0; i < length; i++ )); do
        local c="${1:i:1}"
        case $c in
            [a-zA-Z0-9.~_-]) printf "$c" ;;
            *) printf '%%%02X' "'$c" ;; 
        esac
    done
}
Katorone commented 6 years ago

I'm having an issue deploying this on subdomains. On Godaddy, I execute: acme.sh --deploy -d domain.com -w ~/www -d www.domain.com -w ~/www -d forum.domain.com -w ~/www/forum --deploy-hook cpanel_uapi Yet in the cpanel the entry for the https is only added for domain.com.

Do I have to create several configs for acme.sh?

Neilpang commented 6 years ago

@Katorone

  1. you don't need to give -w parameter for --deploy command, it's useless.
  2. you can only give one -d parameter, all the other -d are ignored.
  3. so, please issue certs for each one of your website, and deploy them one by one.
Katorone commented 6 years ago

Issuing first and deploying after solved the issue I was having. Thank you for your help!