Skip to content

Commit

Permalink
0.0.2 issues #1 #2 #3
Browse files Browse the repository at this point in the history
  • Loading branch information
hypersleep committed Jul 27, 2016
1 parent 56c36cc commit 3c6c2cc
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 96 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 0.0.2

- errors handling #3
- complete readme #2
- better configuration #1
124 changes: 88 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,53 +13,105 @@ At first create following K/V structure in Consul:
```
letsconsul
|_
| \_domains
| |_
| | \_example.com
| | |_
| | | \_domain_list = ["www.example.com", "example.com"]
| | |_
| | | \_email = [email protected]
| | |_
| | | \_timestamp = 0
| | |_
| | | \_private_key =
| | |_
| | \_fullchain =
| |_
| \_qlean.ru
| |_
| | \_domain_list = ["qlean.ru", "www.qlean.ru", "assets.qlean.ru"]
| |_
| | \email = [email protected]
| |_
| | \_timestamp = 0
| |_
| | \_private_key =
| |_
| \_fullchain =
| \_renew_interval = 168h
|_
\_domains_enabled = ["example.com", "qlean.ru"]
| \_reload_interval = 10s
|_
| \_service = letsconsul
|_
| \_domains_enabled = ["example.com", "qlean.ru"]
|_
\_domains
|_
| \_example.com
| |_
| | \_domain_list = ["www.example.com", "example.com"]
| |_
| | \_email = [email protected]
| |_
| | \_timestamp = 0
| |_
| | \_private_key =
| |_
| \_fullchain =
|_
\_qlean.ru
|_
| \_domain_list = ["qlean.ru", "www.qlean.ru", "assets.qlean.ru"]
|_
| \email = [email protected]
|_
| \_timestamp = 0
|_
| \_private_key =
|_
\_fullchain =
```

When letsconsul starting it reading particular environment variables:
Consul configuration keys:

- `renew_interval` - domain certificate expiration time
- `reload_interval` - time after letsconsul reloading domains information from consul
- `service` - consul service name
- `domains_enabled` - domains from `letsconsul/domains` that can be validated and renewed with certs/keys

When letsconsul starting it reading particular command line arguments:

- `-b` - host:port variable that validation web-server will listen (by default 0.0.0.0:8080)
- `-c` - consul address (by default 127.0.0.1:8500)

- `BIND` - host:port variable that server will listen (e.g BIND=0.0.0.0:21234)
- `RENEW_INTERVAL` - domain certificate expiration time (e.g. RENEW_INTERVAL=168h)
- `RELOAD_INTERVAL` - time after letsconsul reloading domains information from consul (e.g. RELOAD_INTERVAL=10s)
- `CONSUL_SERVICE` - consul service name and k/v folder where domains serving (e.g. CONSUL_SERVICE=letsconsul)
- `CONSUL_TOKEN` - consul ACL token
Also, you can specifly consul ACL token with CONSUL_TOKEN environment variable.

Example of usage:

```
$ go build
$ BIND=0.0.0.0:21234 RENEW_INTERVAL=168h RELOAD_INTERVAL=10s CONSUL_SERVICE=letsconsul letsconsul
$ wget https://github.com/hypersleep/letsconsul/releases/download/0.0.2/letsconsul-linux-64.zip
$ unzip letsconsul-linux-64.zip
$ ./letsconsul -b 0.0.0.0:8080 -c 127.0.0.1:8500
```

## Workflow description

After app starts, it fetching domains information from consul by given `consul_service` key, checking certificate expiration time and if more than `renew_interval` then starts certificate renew process.

Updating and receiving certificates is based on LetsEncrypt HTTP validation.

After validation request has sent to LetsEncrypt, letsconsul starts a validation web-server on address `-b` that should receive a LetsEncrypt validation request.

Ensure that validation web-server available from internet! You can simply do this with combination of nginx proxy and consul-template:

```
server {
listen 80;
server_name www.example.com example.com;
{{range service "letsconsul"}}
location /.well-known/acme-challenge {
proxy_set_header Host $host;
proxy_pass http://{{.Address}}:{{.Port}};
}
{{end}}
}
```

If validation will be made successfully, letsconsul writes received certificates and keys to `fullchain` and `private_key` consul keys.

After that you can use values of `fullchain` and `private_key` as cert/key files for nginx using consul-template:

/etc/ssl/example.com.crt.ctmpl:
```
{{key "letsconsul/domains/example.com/fullchain"}}
```
rendering to: /etc/ssl/example.com.crt

/etc/ssl/example.com.key.ctmpl:
```
{{key "letsconsul/domains/example.com/private_key"}}
```
rendering to: /etc/ssl/example.com.key

After app starts, it fetching domains information from consul by given `CONSUL_SERVICE` env variable, checking certificate expiration time and if more than `RENEW_INTERVAL` then starts certificate renew process.
Finally, you're got fully automated and distributed by many servers/proxies HTTPS certificates!

You can see full workflow on following chart:

![Workflow](workflow.png)

134 changes: 83 additions & 51 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import(
"log"
"net"
"time"
"reflect"
"flag"
"errors"
"net/http"
"strconv"
"syscall"
Expand All @@ -17,45 +18,74 @@ import(

type(
App struct {
Bind string `env:"BIND"`
ConsulToken string `env:"CONSUL_TOKEN"`
ConsulService string `env:"CONSUL_SERVICE"`
RenewInterval time.Duration `env:"RENEW_INTERVAL"`
ReloadInterval time.Duration `env:"RELOAD_INTERVAL"`
Bind string
ConsulToken string
ConsulService string `consul:"service"`
RenewInterval time.Duration `consul:"renew_interval"`
ReloadInterval time.Duration `consul:"reload_interval"`
consulClient *consul.Client
consulServiceID string
letsconsul *Letsconsul
}
)

func (app *App) config() error {
structType := reflect.TypeOf(*app)
structValue := reflect.ValueOf(app).Elem()

stringType := reflect.TypeOf(string(""))
durationType := reflect.TypeOf(time.Duration(0))

for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
envKey := field.Tag.Get("env")
if envKey != "" {
envValue := os.Getenv(envKey)

switch field.Type {
case stringType:
structValue.FieldByName(field.Name).SetString(envValue)
case durationType:
duration, err := time.ParseDuration(envValue)
if err != nil {
return err
}
structValue.FieldByName(field.Name).Set(reflect.ValueOf(duration))
}
}
func (app *App) consulConfigure() error {
kv := app.consulClient.KV()
prefix := "letsconsul"

kvPair, _, err := kv.Get(prefix + "/service", nil)
if err != nil {
return err
}

if kvPair == nil {
return errors.New("Can't fetch 'service' key")
}

app.ConsulService = string(kvPair.Value)

kvPair, _, err = kv.Get(prefix + "/renew_interval", nil)
if err != nil {
return err
}

if kvPair == nil {
return errors.New("Can't fetch 'renew_interval' key")
}

app.RenewInterval, err = time.ParseDuration(string(kvPair.Value))
if err != nil {
return err
}

kvPair, _, err = kv.Get(prefix + "/reload_interval", nil)
if err != nil {
return err
}

if kvPair == nil {
return errors.New("Can't fetch 'reload_interval' key")
}

app.ReloadInterval, err = time.ParseDuration(string(kvPair.Value))
if err != nil {
return err
}

return nil
}

func (app *App) config() error {
app.ConsulToken = os.Getenv("CONSUL_TOKEN")

bindPtr := flag.String("b", "0.0.0.0:8080", "host:port variable that validation web-server will listen")
consulAddrPtr := flag.String("c", "127.0.0.1:8500", "consul address")
flag.Parse()

app.Bind = *bindPtr

consulConfig := &consul.Config{
Address: "127.0.0.1:8500",
Address: *consulAddrPtr,
Token: app.ConsulToken,
Scheme: "http",
HttpClient: http.DefaultClient,
Expand All @@ -68,10 +98,21 @@ func (app *App) config() error {

app.consulClient = client

err = app.consulConfigure()
if err != nil {
return err
}

go func() {
for {
time.Sleep(10 * time.Second)
app.consulConfigure()
}
}()

return nil
}


func (app *App) register() error {
app.consulServiceID = uuid.NewV4().String()

Expand Down Expand Up @@ -137,39 +178,30 @@ func (app *App) register() error {
func (app *App) renewDomains() error {
app.letsconsul.Domains = make(map[string]*DomainRecord)

err := app.letsconsul.get(app.consulClient, app.ConsulService)
err := app.letsconsul.get(app.consulClient)
if err != nil {
return err
}

app.letsconsul.Bind = app.Bind

err = app.letsconsul.renew(app.consulClient, app.ConsulService, app.RenewInterval)
err = app.letsconsul.renew(app.consulClient, app.RenewInterval)
if err != nil {
return err
}

return nil
}

func (app *App) start() error {
var errChan chan error = make(chan error)

go func() {
app.letsconsul = &Letsconsul{}

for {
err := app.renewDomains()
if err != nil {
errChan <- err
return
}
func (app *App) start() {
app.letsconsul = &Letsconsul{}

<- time.After(app.ReloadInterval)
for {
err := app.renewDomains()
if err != nil {
log.Println(err)
}
}()

log.Println("Application loaded")

return <- errChan
<- time.After(app.ReloadInterval)
}
}
3 changes: 2 additions & 1 deletion domain_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ func (domainRecord *DomainRecord) GetEmail() string { return domainRecord.Email
func (domainRecord *DomainRecord) GetRegistration() *acme.RegistrationResource { return domainRecord.Reg }
func (domainRecord *DomainRecord) GetPrivateKey() crypto.PrivateKey { return domainRecord.key }

func (domainRecord *DomainRecord) write(client *consul.Client, consulService string, domainRecordName string) error {
func (domainRecord *DomainRecord) write(client *consul.Client, domainRecordName string) error {
kv := client.KV()
consulService := "letsconsul"

timestamp := domainRecord.Timestamp.Unix()
timestampStr := strconv.Itoa(int(timestamp))
Expand Down
7 changes: 4 additions & 3 deletions letsconsul.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ type(
}
)

func (letsconsul *Letsconsul) renew(client *consul.Client, consulService string, renewInterval time.Duration) error {
func (letsconsul *Letsconsul) renew(client *consul.Client, renewInterval time.Duration) error {
for domainRecordName, domainRecord := range letsconsul.Domains {
if domainRecord.Timestamp.Add(renewInterval).Before(time.Now()) {
err := domainRecord.renew(letsconsul.Bind)
if err != nil {
return err
}

err = domainRecord.write(client, consulService, domainRecordName)
err = domainRecord.write(client, domainRecordName)
if err != nil {
return err
}
Expand All @@ -32,8 +32,9 @@ func (letsconsul *Letsconsul) renew(client *consul.Client, consulService string,
return nil
}

func (letsconsul *Letsconsul) get(client *consul.Client, prefix string) error {
func (letsconsul *Letsconsul) get(client *consul.Client) error {
kv := client.KV()
prefix := "letsconsul"

kvPair, _, err := kv.Get(prefix + "/domains_enabled", nil)
if err != nil {
Expand Down
7 changes: 2 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ func main() {
log.Fatal(err)
}

log.Println("Loading application")
log.Println("Application loaded")

err = app.start()
if err != nil {
log.Fatal(err)
}
app.start()
}

0 comments on commit 3c6c2cc

Please sign in to comment.