Skip to content

Commit

Permalink
Merge pull request #565 from TheThingsNetwork/feature/acme
Browse files Browse the repository at this point in the history
Add Let's Encrypt support
  • Loading branch information
johanstokking authored May 10, 2019
2 parents 3e26659 + c9591e3 commit 288b090
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 5 deletions.
3 changes: 3 additions & 0 deletions cmd/internal/shared/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ var DefaultLogConfig = config.Log{
var DefaultTLSConfig = config.TLS{
Certificate: "cert.pem",
Key: "key.pem",
ACME: config.ACME{
Endpoint: "https://acme-v01.api.letsencrypt.org/directory",
},
}

// DefaultClusterConfig is the default cluster configuration.
Expand Down
18 changes: 18 additions & 0 deletions config/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2069,6 +2069,24 @@
"file": "listeners.go"
}
},
"error:pkg/component:missing_acme_dir": {
"translations": {
"en": "missing ACME storage directory"
},
"description": {
"package": "pkg/component",
"file": "acme.go"
}
},
"error:pkg/component:missing_acme_endpoint": {
"translations": {
"en": "missing ACME endpoint"
},
"description": {
"package": "pkg/component",
"file": "acme.go"
}
},
"error:pkg/config:format": {
"translations": {
"en": "invalid format `{input}`"
Expand Down
11 changes: 9 additions & 2 deletions doc/gettingstarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,14 @@ If your operating system or package manager is not mentioned, please [download b

By default, the stack requires a `cert.pem` and `key.pem`, in order to to serve content over TLS.

Typically you'll get these from a trusted Certificate Authority. We recommend [Let's Encrypt](https://letsencrypt.org/getting-started/) for free and trusted TLS certificates for your server. Use the "full chain" for `cert.pem` and the "private key" for `key.pem`.
Typically you'll get these from a trusted Certificate Authority. Use the "full chain" for `cert.pem` and the "private key" for `key.pem`. The stack also has support for automated certificate management (ACME). This allows you to easily get trusted TLS certificates for your server from [Let's Encrypt](https://letsencrypt.org/getting-started/). If you want this, you'll need to create an `acme` directory that the stack can write in:

```bash
$ mkdir ./acme
$ chown 886:886 ./acme
```

> If you don't do this, you'll get an error saying something like `open /var/lib/acme/acme_account+key<...>: permission denied`.
For local (development) deployments, you can generate self-signed certificates. If you have your [Go environment](../DEVELOPMENT.md#development-environment) set up, you can run the following command to generate a key and certificate for `localhost`:

Expand All @@ -73,7 +80,7 @@ Keep in mind that self-signed certificates are not trusted by browsers and opera

## <a name="configuration">Configuration</a>

The stack can be started without passing any configuration. However, there are a lot of things you can configure. See [configuration documentation](config.md) for more information.
The stack can be started for development without passing any configuration. However, there are a lot of things you can configure. See [configuration documentation](config.md) for more information. In this guide we'll set some environment variables in `docker-compose.yml`. These environment variables will configure the stack as a development server on `localhost`. For setting up up a public server or for requesting certificates from an ACME provider such as Let's Encrypt, take a closer look at the comments in `docker-compose.yml`.

Refer to the [networking documentation](networking.md) for the endpoints and ports that the stack uses by default.

Expand Down
21 changes: 20 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,35 @@ services:
- TTN_LW_HTTP_COOKIE_BLOCK_KEY
- TTN_LW_CLUSTER_KEYS
- TTN_LW_FREQUENCY_PLANS_URL
- TTN_LW_CONSOLE_OAUTH_CLIENT_SECRET
# If using CockroachDB:
- TTN_LW_IS_DATABASE_URI=postgres://root@cockroach:26257/${DEV_DATABASE_NAME:-ttn_lorawan}?sslmode=disable
# If using PostgreSQL:
# - TTN_LW_IS_DATABASE_URI=postgres://root@postgres:5432/${DEV_DATABASE_NAME:-ttn_lorawan}?sslmode=disable
- TTN_LW_REDIS_ADDRESS=redis:6379
# If using (self) signed certificates:
- TTN_LW_TLS_CERTIFICATE=/run/secrets/cert.pem
- TTN_LW_CA=/run/secrets/cert.pem
- TTN_LW_TLS_KEY=/run/secrets/key.pem
# If using Let's Encrypt for "example.com":
# - TTN_LW_TLS_ACME_DIR=/var/lib/acme
# - [email protected]
# - TTN_LW_TLS_ACME_ENABLE=true
# - TTN_LW_TLS_ACME_HOSTS=example.com
# If it's a public server on "example.com":
# - TTN_LW_IS_EMAIL_NETWORK_IDENTITY_SERVER_URL=https://example.com/oauth
# - TTN_LW_IS_OAUTH_UI_CANONICAL_URL=https://example.com/oauth
# - TTN_LW_HTTP_METRICS_PASSWORD=<set a password>
# - TTN_LW_HTTP_PPROF_PASSWORD=<set a password>
depends_on:
- redis
# If using CockroachDB:
- cockroach
# If using PostgreSQL:
# - postgres
volumes:
- ./blob:/srv/ttn-lorawan/public/blob
# If using Let's Encrypt:
# - ./acme:/var/lib/acme
ports:
- "1882:1882"
- "8882:8882"
Expand All @@ -33,7 +47,11 @@ services:
- "8884:8884"
- "1885:1885"
- "8885:8885"
# If deploying on a public server:
# - "80:1885"
# - "443:8885"
- "1700:1700/udp"
# If using (self) signed certificates:
secrets:
- cert.pem
- key.pem
Expand Down Expand Up @@ -64,6 +82,7 @@ services:
- ${DEV_DATA_DIR:-.env/data}/redis:/data
ports:
- "127.0.0.1:6379:6379"
# If using (self) signed certificates:
secrets:
cert.pem:
file: ./cert.pem
Expand Down
2 changes: 1 addition & 1 deletion pkg/basicstation/cups/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (conf ServerConfig) NewServer(c *component.Component, customOpts ...Option)
})
}))
}
if tlsConfig, err := c.GetBaseConfig(c.Context()).TLS.Config(c.Context()); err == nil {
if tlsConfig, err := c.GetTLSConfig(c.Context()); err == nil {
opts = append(opts, WithRootCAs(tlsConfig.RootCAs))
}
s := NewServer(c, append(opts, customOpts...)...)
Expand Down
70 changes: 70 additions & 0 deletions pkg/component/acme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright © 2019 The Things Network Foundation, The Things Industries B.V.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package component

import (
"context"
"crypto/tls"

echo "github.com/labstack/echo/v4"
"go.thethings.network/lorawan-stack/pkg/errors"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
)

var (
errMissingACMEDir = errors.Define("missing_acme_dir", "missing ACME storage directory")
errMissingACMEEndpoint = errors.Define("missing_acme_endpoint", "missing ACME endpoint")
)

func (c *Component) initACME() error {
if !c.config.TLS.ACME.Enable {
return nil
}
if c.config.TLS.ACME.Endpoint == "" {
return errMissingACMEEndpoint
}
if c.config.TLS.ACME.Dir == "" {
return errMissingACMEDir
}
c.acme = &autocert.Manager{
Cache: autocert.DirCache(c.config.TLS.ACME.Dir),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(c.config.TLS.ACME.Hosts...),
Client: &acme.Client{
DirectoryURL: c.config.TLS.ACME.Endpoint,
},
Email: c.config.TLS.ACME.Email,
}
c.acmeTLS = &tls.Config{
GetCertificate: c.acme.GetCertificate,
PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS12,
NextProtos: []string{
"h2", "http/1.1",
acme.ALPNProto,
},
}
c.web.Any(".well-known/acme-challenge/*", echo.WrapHandler(c.acme.HTTPHandler(nil)))
return nil
}

// GetTLSConfig gets the component's TLS config.
func (c *Component) GetTLSConfig(ctx context.Context) (*tls.Config, error) {
if c.acmeTLS != nil {
return c.acmeTLS, nil
}
return c.config.TLS.Config(ctx)
}
9 changes: 9 additions & 0 deletions pkg/component/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package component

import (
"context"
"crypto/tls"
"fmt"
"os"
"os/signal"
Expand All @@ -36,6 +37,7 @@ import (
"go.thethings.network/lorawan-stack/pkg/rpcserver"
"go.thethings.network/lorawan-stack/pkg/version"
"go.thethings.network/lorawan-stack/pkg/web"
"golang.org/x/crypto/acme/autocert"
"google.golang.org/grpc"
)

Expand All @@ -53,6 +55,9 @@ type Component struct {
config *Config
getBaseConfig func(ctx context.Context) config.ServiceBase

acme *autocert.Manager
acmeTLS *tls.Config // TLS configuration if ACME is used.

logger log.Stack
sentry *raven.Client

Expand Down Expand Up @@ -145,6 +150,10 @@ func New(logger log.Stack, config *Config, opts ...Option) (*Component, error) {
return nil, err
}

if err := c.initACME(); err != nil {
return nil, err
}

c.initRights()

c.initGRPC()
Expand Down
2 changes: 1 addition & 1 deletion pkg/component/listeners.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (l *listener) TLS() (net.Listener, error) {
if l.tlsUsed {
return nil, errors.New("TLS listener already in use")
}
config, err := l.c.config.TLS.Config(l.c.Context())
config, err := l.c.GetTLSConfig(l.c.Context())
if err != nil {
return nil, err
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/config/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ type TLS struct {
RootCA string `name:"root-ca" description:"Location of TLS root CA certificate (optional)"`
Certificate string `name:"certificate" description:"Location of TLS certificate"`
Key string `name:"key" description:"Location of TLS private key"`
ACME ACME `name:"acme"`
}

// ACME represents ACME configuration.
type ACME struct {
Enable bool `name:"enable" description:"Enable automated certificate management (ACME)"`
Endpoint string `name:"endpoint" description:"ACME endpoint"`
Dir string `name:"dir" description:"Location of ACME storage directory"`
Email string `name:"email" description:"Email address to register with the ACME account"`
Hosts []string `name:"hosts" description:"Hosts to enable automatic certificates for"`
}

var errNoKeyPair = errors.DefineFailedPrecondition("no_key_pair", "no TLS key pair")
Expand Down

0 comments on commit 288b090

Please sign in to comment.