/ Certbot

LetsEncrypt + Salesforce Communities

Part 1: So, as a proof of concept, I was interested in setting up a Salesforce Customer Community, with a custom domain and use an SSL certificate from LetsEncrypt, since LetsEncrypt is free.

The first thing I had to do was set up a custom domain. Thankfully, I already own a couple of domains, so that wasn't a big deal. Getting the cert while the domain hostname points to Salesforce is another thing altogether.

This is where CertBot Manual Mode comes in. Using manual mode allows me to have certbot (the LetsEncrypt cert client I chose to use) check for a "file" on the web server. This was actually not difficult to solve. The file has to live at a certain path: /.well-known/acme-challenge/[alpha-numeric prefix] and must contain a certain string value. Coincidentally, this string is a combination of the [alpha-numeric prefix] in the url as well as a constant suffix, concatenated together.

The solution was to create a URLRewrite class that returns a visualforce page that takes the prefix in the url and returns that prefix concatenated with the suffix which is stored as a custom setting in the salesforce org. Because that suffix is always the same (encrypted/hashed domain hostname value, I'm guessing), this method works.

URLRewrite Class

global class CertBotUrlRewrite implements Site.UrlRewriter {

    string prefix = '/.well-known/acme-challenge/';

    global PageReference mapRequestUrl(PageReference friendlyUrl) {           
        string url = friendlyUrl.getUrl();
        if(url.startsWith(prefix)){
            string suffix = url.substring(prefix.length(), url.length());
            return new PageReference('certbot_response?suffix=' + suffix);
        }                    
        return null;
    }
    
    global PageReference[] generateUrlFor(PageReference[] yourSalesforceUrls) {
        return null;
    }
}

VisualForce Controller

public class certbotController {

    public certbotController() {
    
    }

    public String getRequestResponse() {
        string suffix = Apexpages.currentPage().getParameters().get('suffix');
        return suffix + certbot__c.getOrgDefaults().Value__c;
    }
}

VisualForce Page

<apex:page 
    applyHtmlTag="false" 
    applyBodyTag="false" 
    showHeader="false" 
    standardStylesheets="false" 
    sidebar="false" 
    contentType="text/plain" 
    controller="certbotController">{!RequestResponse}</apex:page>

This enabled me to generate LetsEncrypt certs using a custom domain pointing to a Salesforce Community. Obviously, you have to set up anonymous page access for that VF page and enable the community guest profile in question to be able to view it (basic SFDC stuff).

Ok, I have my certs now. How do I get them into SFDC? Salesforce requires a JKS (Java KeyStore) if you want to import your own certificate (chain) into the platform. I first tried doing this with Cloudflare Origin certs, but this was a no go. The Cloudflare Origin CA is not a globally trusted CA, so, not dice. I mean, I was able to upload it and associate it with the SFDC Community, but I ended up with that ugly Insecure Website certificate warning.

Update: Part 2 describes how to script the process below

Anyway, the key to getting the cert into SFDC is to create a JKS file. In order to do that I had to first use openssl to generate a PKCS12 bundle that I would then import into a new JKS. So we have a couple of CLI commands to do this:

$ openssl pkcs12 -export \
    -in [filename-certificate] \
    -inkey [filename-key] \
    -name [host] \
    -out [filename-new-PKCS-12.p12]

$ keytool -importkeystore \
    -srckeystore [filename-new-PKCS-12.p12] \
    -destkeystore cert.jks \
    -srcstoretype pkcs12

This then prompts you for two passwords, one for the key and one for the store.

Once this is done, uploading the JSK file into SFDC and associating it with the custom URL for the community was simple admin work.

Next step is attempting to automate this process with Jenkins and the SFDC Metadata API.

All in all, though, I was pleased with the results.