Browse Source

Various fixes, improvements.

master
Ruel Tmeizeh 4 years ago
parent
commit
2af4fe681b
3 changed files with 184 additions and 64 deletions
  1. +47
    -0
      README.md
  2. +5
    -2
      leapi_config.json
  3. +132
    -62
      main.go

+ 47
- 0
README.md View File

@ -0,0 +1,47 @@
# LEAPI
LEAPI is a clustered server API system, written in Go, for managing Lets Encrypt certificate renewals.
LEAPI uses the excellent [getssl](https://github.com/srvrco/getssl) Bash script for the actual renewal of certificates.
It can be used on a single server, but is particularly useful for clusters of servers, with many domains.
You can use it standalone, for acquiring/renewing certificates for non web services, or with an external webserver like Nginx, Caddy, etc.
LEAPI operates in a multi-master configuration. When you add or delete a server or domain on any server, it automatically replicates the changes to all other servers, and renews your certificate. Replication is accomplished via HTTP.
## Endpoints:
```[GET] https://leapiserver.tld/api/servers``` --- List Servers
```[PUT] https://leapiserver.tld/api/servers/web1.mydomain.com``` --- Add New Server
```[GET] https://leapiserver.tld/api/domains``` --- List Domains
```[POST] https://leapiserver.tld/api/domains/mycoolsite.com``` --- Add New Domain
```[POST] https://leapiserver.tld/api/renew``` --- Force Renewal
```[GET] https://leapiserver.tld/up``` --- Uptime Check
## Install
- Download the LEAPI binary, or build from source.
- Copy it to ```/opt/leapi```
- You may use the included SystemD service file if you use a SystemD based distribution.
- Edit the ```leapi_config.json``` file for your needs, leaving ```production``` set to ```false``` until setup is complete, and copy it to ```/opt/leapi``` or ```/etc```.
- Install getssl with ```curl --silent https://raw.githubusercontent.com/srvrco/getssl/latest/getssl > /opt/leapi/getssl ; chmod 700 /opt/leapi/getssl```
- Create the base config for getssl: ```/opt/leapi/getssl -w /opt/leapi -c mycoolsite.com```
- Start LEAPI, either from the commandline or with ```systemctl start leapi```
- Add your servers via the LEAPI API: (You don't necessarily have to do this on the server itself.)
curl -X PUT http://localhost/api/servers/server1.mydomain.com -H 'Authorization: Bearer mySeCrEtKeY'
curl -X PUT http://localhost/api/servers/server2.mydomain.com -H 'Authorization: Bearer mySeCrEtKeY'
curl -X PUT http://localhost/api/servers/server3.mydomain.com -H 'Authorization: Bearer mySeCrEtKeY'
- Add your domains via the LEAPI API:
curl -X PUT http://localhost/api/domains/mycoolsite.com -H 'Authorization: Bearer mySeCrEtKeY'
curl -X PUT http://localhost/api/domains/myothersite.com -H 'Authorization: Bearer mySeCrEtKeY'
- Assuming there were no errors, edit your ```leapi_config.json``` file and change ```production``` to ```true```.
- Force a renewal via the API:
curl -X POST http://localhost/api/renew -H 'Authorization: Bearer mySeCrEtKeY'

+ 5
- 2
leapi_config.json View File

@ -7,7 +7,7 @@
"user":"root", //the username to use for SSH
"log_file":"/var/log/leapi.log",
"frontend_url":"admin.mydomain.net", //the frontend URL, if any (for CORS). Use "-" if none.
"http_server_port":"8080", //set to 80 if you aren't using a separate web server
"http_server_port":"80", //set to 80 if you aren't using a separate web server
"https_server_port":"-", //set to "-" to disable HTTPS (mainly useful for initial setup)
"tls_cert_path":"/etc/ssl/cert.crt",
"tls_key_path":"/etc/ssl/privkey.key",
@ -16,7 +16,10 @@
"tls_ca_path":"/etc/ssl/ca.crt",
"letsencrypt_validation_path":"-", //if "-", LEAPI handles this and you don't use a separate web server
"renew_allow_days":"70",
"reload_command":"systemctl reload leapi ; systemctl restart nginx" //needs to match on all servers
"reload_command":"systemctl reload leapi ; systemctl restart nginx", //needs to match on all servers
"check_port":"443", //the port/service to check to verify cert installation (https/imap/imaps/xmpp/ftp/smtp)
"production":false, //if false, the staging LE server will be used. Set true to use the rate limited real server.
"secret_key":"jEn-vQ832h^01j2rUq0jd-svji8ejf"
}

+ 132
- 62
main.go View File

@ -72,6 +72,9 @@ type LEAPIConfig struct {
LetsEncryptValidationPath string `json:"letsencrypt_validation_path"`
ReloadCommand string `json:"reload_command"`
RenewAllow string `json:"renew_allow_days"`
SecretKey string `json:"secret_key"`
Production bool `json:"production"`
CheckPort string `json:"check_port"`
}
type UpOut struct {
@ -205,10 +208,17 @@ func main() {
}))
*/
/////////////////////////////////////////////
// ROUTE GROUPS
api := e.Group("/api") //API routes
/////////////////////////////////////////////
/////////////////////////////////////////////
// MIDDLEWARE
//Add server header and CORS
e.Use(serverHeaders)
//Auth API routes
api.Use(middleware.KeyAuth(apiKeyAuth))
/////////////////////////////////////////////
/////////////////////////////////////////////
@ -226,23 +236,23 @@ func main() {
// API Routes //
/////////////////////////////////
e.OPTIONS("/domains", apiListDomains)
e.GET("/domains", apiListDomains)
e.OPTIONS("/domains/:domain", apiPutDomain)
e.PUT("/domains/:domain", apiPutDomain)
e.DELETE("/domains/:domain", apiDeleteDomain)
api.OPTIONS("/domains", apiListDomains)
api.GET("/domains", apiListDomains)
api.OPTIONS("/domains/:domain", apiPutDomain)
api.PUT("/domains/:domain", apiPutDomain)
api.DELETE("/domains/:domain", apiDeleteDomain)
e.OPTIONS("/servers", apiListServers)
e.GET("/servers", apiListServers)
e.OPTIONS("/servers/:server", apiPutServer)
e.PUT("/servers/:server", apiPutServer)
e.DELETE("/servers/:server", apiDeleteServer)
api.OPTIONS("/servers", apiListServers)
api.GET("/servers", apiListServers)
api.OPTIONS("/servers/:server", apiPutServer)
api.PUT("/servers/:server", apiPutServer)
api.DELETE("/servers/:server", apiDeleteServer)
e.OPTIONS("/sync/:host", apiSync)
e.POST("/sync/:host", apiSync)
api.OPTIONS("/sync/:host", apiSync)
api.POST("/sync/:host", apiSync)
e.OPTIONS("/renew", apiRenew)
e.POST("/renew", apiRenew)
api.OPTIONS("/renew", apiRenew)
api.POST("/renew", apiRenew)
/////////////////////////////////////////////
// HTTP SERVERS CONFIG:
@ -347,6 +357,10 @@ func serverHeaders(next echo.HandlerFunc) echo.HandlerFunc {
}
}
func apiKeyAuth(key string, c echo.Context) (bool, error) {
return (key == leapiconf.SecretKey), nil
}
func NewKeypairReloader(certPath, keyPath string) (*keypairReloader, error) {
result := &keypairReloader{
certPath: certPath,
@ -445,55 +459,88 @@ func writeServers() error {
func syncAllServers() error {
var theError error
for _, server := range servers {
if server == leapiconf.Hostname { //don't send request to myself
numservers := len(servers)
c := make(chan string)
var wg sync.WaitGroup
wg.Add(numservers)
for n := 0; n < numservers; n++ {
go func(c chan string) {
for {
srv, more := <-c
if more == false {
wg.Done()
return
}
log.Println("Parallel execution sync of server: " + srv + "...")
err := syncOneServer(srv)
if err != nil {
log.Println(err.Error)
theError = err
}
}
}(c)
}
for _, server := range servers { //send each server to the channel
if server == leapiconf.Hostname { //don't send myself
continue
}
//Make http requests to each other servers' /sync endpoints
// https://server.tld:port/sync
req, err := http.NewRequest("POST", syncScheme+server+":"+syncPort+"/sync/"+leapiconf.Hostname, nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP sync request for server: " + server)
}
req.Close = true
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", myUserAgent)
//skip verification of cert for https syncing, since the cert may not be setup properly at first
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{Transport: customTransport, Timeout: timeout}
//client := &http.Client{Timeout: timeout}
response, err := client.Do(req)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't perform HTTP sync request to server: " + server)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body on request to server: " + server)
}
if response.StatusCode != 200 {
theError = errors.New("Problem syncing to server " + server + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body))
log.Println(theError.Error())
}
c <- server
}
if theError != nil {
return theError
close(c)
wg.Wait()
log.Println("Finished sending sync requests.")
return theError //if any one or more fail, return an error for it (the last one that fails)
}
func syncOneServer(server string) error {
//Make http requests to each other servers' /sync endpoints
// https://server.tld:port/sync
log.Println("SYNC " + server + " starting...")
req, err := http.NewRequest("POST", syncScheme+server+":"+syncPort+"/api/sync/"+leapiconf.Hostname, nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP sync request for server: " + server)
}
req.Close = true
req.Header.Set("User-Agent", myUserAgent)
req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
//skip verification of cert for https syncing, since the cert may not be setup properly at first
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{Transport: customTransport, Timeout: timeout}
//client := &http.Client{Timeout: timeout}
response, err := client.Do(req)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't perform HTTP sync request to server: " + server)
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't parse response body on request to server: " + server)
}
if response.StatusCode != 200 {
errorString := "Problem syncing to server " + server + ". Status code: " + strconv.Itoa(response.StatusCode) + " Body: " + string(body)
log.Println(errorString)
return errors.New(errorString)
}
log.Println("SYNC " + server + " success!")
return nil
}
func syncServersFromHost(host string) error {
var theError error
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/servers", nil)
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/api/servers", nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP request for syncing servers from host: " + host)
}
req.Close = true
req.Header.Set("User-Agent", myUserAgent)
req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
//skip verification of cert for https syncing, since the cert may not be setup properly at first
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
@ -534,13 +581,14 @@ func syncServersFromHost(host string) error {
func syncDomainsFromHost(host string) error {
var theError error
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/domains", nil)
req, err := http.NewRequest("GET", syncScheme+host+":"+syncPort+"/api/domains", nil)
if err != nil {
log.Println(err.Error())
return errors.New("Couldn't create new HTTP request for syncing domains from host: " + host)
}
req.Close = true
req.Header.Set("User-Agent", myUserAgent)
req.Header.Set("Authorization", "Bearer "+leapiconf.SecretKey)
//skip verification of cert for https syncing, since the cert may not be setup properly at first
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
@ -580,6 +628,7 @@ func syncDomainsFromHost(host string) error {
}
func renew() error {
log.Println("Renew operation initiated...")
//BUILD/SET GETSSL ENVIRONMENT VARIABLES THEN EXECUTE GETSSL
//domain list
@ -663,6 +712,7 @@ func renew() error {
return errors.New("RENEW: error setting DOMAIN_PEM_LOCATION environment variable: " + err.Error())
}
//these parameters don't seem to be respected by gettssl from environment variables, so write them to config file:
ca_cert_location := leapiconf.TLSCAFile
for _, server := range servers {
if server == leapiconf.Hostname {
@ -670,42 +720,60 @@ func renew() error {
}
ca_cert_location += ";ssh:" + leapiconf.Username + "@" + server + ":" + leapiconf.TLSCAFile
}
err = os.Setenv("CA_CERT_LOCATION", ca_cert_location)
if err != nil {
return errors.New("RENEW: error setting CA_CERT_LOCATION environment variable: " + err.Error())
}
//reload command
reload_command := leapiconf.ReloadCommand
for _, server := range servers {
if server == leapiconf.Hostname {
continue
}
reload_command += "; ssh " + leapiconf.Username + "@" + server + " '" + leapiconf.ReloadCommand + "' "
reload_command += "; ssh " + leapiconf.Username + "@" + server + " '" + leapiconf.ReloadCommand + "'"
}
err = os.Setenv("RELOAD_CMD", reload_command)
if err != nil {
return errors.New("RENEW: error setting RELOAD_COMMAND environment variable: " + err.Error())
ca_server := "https://acme-staging-v02.api.letsencrypt.org"
if leapiconf.Production {
ca_server = "https://acme-v02.api.letsencrypt.org"
}
fmt.Println(reload_command)
err = os.Setenv("RENEW_ALLOW", leapiconf.RenewAllow)
var configFile string
configFile = "CA=\"" + ca_server + "\"\n"
configFile += "USE_SINGLE_ACL=\"true\"\n"
configFile += "CA_CERT_LOCATION=\"" + leapiconf.TLSCAFile + "\"\n"
configFile += "RELOAD_CMD=\"" + reload_command + "\"\n"
configFile += "RENEW_ALLOW=\"" + leapiconf.RenewAllow + "\"\n"
configFile += "CHECK_REMOTE=\"true\"\n"
configFile += "SERVER_TYPE=\"" + leapiconf.CheckPort + "\"\n"
configFile += "CHECK_REMOTE_WAIT=\"5\"\n"
//write config file
err = ioutil.WriteFile(configDir+"/"+leapiconf.PrimaryDomain+"/getssl.cfg", []byte(configFile), 0644)
if err != nil {
return errors.New("RENEW: error setting RENEW_ALLOW environment variable: " + err.Error())
return errors.New("Couldn't write getssl config file: " + configDir + "/" + leapiconf.PrimaryDomain + "/getssl.cfg")
}
/*
//////PRINT VARS
fmt.Println()
for _, e := range os.Environ() {
fmt.Println(e)
}
*/
//RUN GETSSL
//run getssl on primary domain to renew
//cmd := exec.Command(leapiconf.SrvDir+"/getssl", "-u", "-w", leapiconf.SrvDir, leapiconf.PrimaryDomain)
cmd := exec.Command(leapiconf.SrvDir+"/getssl", "-w", leapiconf.SrvDir, leapiconf.PrimaryDomain)
output, err := cmd.CombinedOutput()
if err != nil {
log.Println("BEGIN GETSSL OUTPUT:")
log.Println(string(output))
log.Println("END GETSSL OUTPUT")
return errors.New("RENEW: execution of getssl failed: " + err.Error())
}
log.Println("BEGIN GETSSL OUTPUT:")
log.Println(string(output))
log.Println("END GETSSL OUTPUT:")
log.Println("END GETSSL OUTPUT")
return nil
}
@ -904,6 +972,8 @@ func apiDeleteServer(c echo.Context) error {
func apiSync(c echo.Context) error {
host := c.Param("host")
log.Println("Received sync request for host: " + host + ". From IP address: " + c.RealIP() + " Syncing...")
err := syncServersFromHost(host)
if err != nil {
log.Println(err.Error())


Loading…
Cancel
Save