Skip to content

Commit 8ed74e9

Browse files
authored
Merge pull request #19 from arangodb-helper/ssl
Adding SSL support
2 parents fbd5e7a + cafb03c commit 8ed74e9

File tree

7 files changed

+254
-7
lines changed

7 files changed

+254
-7
lines changed

README.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,65 @@ Usually one would use the Docker image `arangodb/arangodb`.
134134
`containerName` is the name of a Docker container that is used to run the
135135
executable. This argument is required when running the executable in docker.
136136

137+
Authentication options
138+
----------------------
139+
140+
The arango starter by default creates a cluster that uses no authentication.
141+
142+
To create a cluster that uses authentication, create a file containing a random JWT secret (single line)
143+
and pass it through the `--jwtSecretFile` option.
144+
145+
For example:
146+
147+
```
148+
echo "MakeThisSecretMuchStronger" > jwtSecret
149+
arangodb --jwtSecretFile=./jwtSecret
150+
```
151+
152+
All starters used in the cluster must have the same JWT secret.
153+
154+
SSL options
155+
-----------
156+
157+
The arango starter by default creates a cluster that uses no unencrypted connections (no SSL).
158+
159+
To create a cluster that uses encrypted connections, you can use an existing server key file
160+
or let the starter create one for you.
161+
162+
To use an existing server key file use the `--sslKeyFile` option like this:
163+
164+
```
165+
arangodb --sslKeyFile=myServer.key
166+
```
167+
168+
Go to the [SSL manual](https://docs.arangodb.com/3.1/Manual/Administration/Configuration/SSL.html) for more
169+
information on how to create a server key file.
170+
171+
To let the starter created a self-signed server key file, use the `--sslAutoKeyFile` option like this:
172+
173+
```
174+
arangodb --sslAutoKeyFile
175+
```
176+
177+
All starters used to make a cluster must be using SSL or not.
178+
You cannot have one starter using SSL and another not using SSL.
179+
180+
Note that all starters can use different server key files.
181+
182+
Additional SSL options:
183+
184+
* `--sslCAFile path`
185+
186+
Configure the servers to require a client certificate in their communication to the servers using the CA certificate in a file with given path.
187+
188+
* `--sslAutoServerName name`
189+
190+
name of the server that will be used in the self-signed certificate created by the `--sslAutoKeyFile` option.
191+
192+
* `--sslAutoOrganization name`
193+
194+
name of the server that will be used in the self-signed certificate created by the `--sslAutoKeyFile` option.
195+
137196
Esoteric options
138197
----------------
139198

@@ -222,8 +281,6 @@ Future plans
222281

223282
* bundle this program with the usual distribution
224283
* make port usage configurable
225-
* support SSL
226-
* support authentication
227284

228285
Technical explanation as to what happens
229286
----------------------------------------

main.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ var (
4949
serverThreads int
5050
allPortOffsetsUnique bool
5151
jwtSecretFile string
52+
sslKeyFile string
53+
sslAutoKeyFile bool
54+
sslAutoServerName string
55+
sslAutoOrganization string
56+
sslCAFile string
5257
dockerEndpoint string
5358
dockerImage string
5459
dockerUser string
@@ -82,6 +87,11 @@ func init() {
8287
f.BoolVar(&dockerPrivileged, "dockerPrivileged", false, "Run containers with --privileged")
8388
f.BoolVar(&allPortOffsetsUnique, "uniquePortOffsets", false, "If set, all peers will get a unique port offset. If false (default) only portOffset+peerAddress pairs will be unique.")
8489
f.StringVar(&jwtSecretFile, "jwtSecretFile", "", "name of a plain text file containing a JWT secret used for server authentication")
90+
f.StringVar(&sslKeyFile, "sslKeyFile", "", "path of a PEM encoded file containing a server certificate + private key")
91+
f.StringVar(&sslCAFile, "sslCAFile", "", "path of a PEM encoded file containing a CA certificate used for client authentication")
92+
f.BoolVar(&sslAutoKeyFile, "sslAutoKeyFile", false, "If set, a self-signed certificate will be created and used as --sslKeyFile")
93+
f.StringVar(&sslAutoServerName, "sslAutoServerName", "", "Server name put into self-signed certificate. See --sslAutoKeyFile")
94+
f.StringVar(&sslAutoOrganization, "sslAutoOrganization", "ArangoDB", "Organization name put into self-signed certificate. See --sslAutoKeyFile")
8595
}
8696

8797
// handleSignal listens for termination signals and stops this process onup termination.
@@ -204,6 +214,30 @@ func cmdMainRun(cmd *cobra.Command, args []string) {
204214
jwtSecret = strings.TrimSpace(string(content))
205215
}
206216

217+
// Auto create key file (if needed)
218+
if sslAutoKeyFile {
219+
if sslKeyFile != "" {
220+
log.Fatalf("Cannot specify both --sslAutoKeyFile and --sslKeyFile")
221+
}
222+
hosts := []string{"arangod.server"}
223+
if sslAutoServerName != "" {
224+
hosts = []string{sslAutoServerName}
225+
}
226+
if ownAddress != "" {
227+
hosts = append(hosts, ownAddress)
228+
}
229+
keyFile, err := service.CreateCertificate(service.CreateCertificateOptions{
230+
Hosts: hosts,
231+
RSABits: 2048,
232+
Organization: sslAutoOrganization,
233+
}, dataDir)
234+
if err != nil {
235+
log.Fatalf("Failed to create keyfile: %v", err)
236+
}
237+
sslKeyFile = keyFile
238+
log.Infof("Using self-signed certificate: %s", sslKeyFile)
239+
}
240+
207241
// Interrupt signal:
208242
sigChannel := make(chan os.Signal)
209243
rootCtx, cancel := context.WithCancel(context.Background())
@@ -227,6 +261,8 @@ func cmdMainRun(cmd *cobra.Command, args []string) {
227261
ServerThreads: serverThreads,
228262
AllPortOffsetsUnique: allPortOffsetsUnique,
229263
JwtSecret: jwtSecret,
264+
SslKeyFile: sslKeyFile,
265+
SslCAFile: sslCAFile,
230266
RunningInDocker: os.Getenv("RUNNING_IN_DOCKER") == "true",
231267
DockerContainer: dockerContainer,
232268
DockerEndpoint: dockerEndpoint,

service/arangodb.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package service
33
import (
44
"context"
55
"crypto/rand"
6+
"crypto/tls"
67
"encoding/hex"
78
"fmt"
89
"io/ioutil"
@@ -35,6 +36,8 @@ type ServiceConfig struct {
3536
ServerThreads int // If set to something other than 0, this will be added to the commandline of each server with `--server.threads`...
3637
AllPortOffsetsUnique bool // If set, all peers will get a unique port offset. If false (default) only portOffset+peerAddress pairs will be unique.
3738
JwtSecret string
39+
SslKeyFile string // Path containing an x509 certificate + private key to be used by the servers.
40+
SslCAFile string // Path containing an x509 CA certificate used to authenticate clients.
3841

3942
DockerContainer string // Name of the container running this process
4043
DockerEndpoint string // Where to reach the docker daemon
@@ -131,13 +134,27 @@ func slasher(s string) string {
131134
return strings.Replace(s, "\\", "/", -1)
132135
}
133136

137+
// IsSecure returns true when the cluster is using SSL for connections, false otherwise.
138+
func (s *Service) IsSecure() bool {
139+
return s.SslKeyFile != ""
140+
}
141+
134142
func (s *Service) testInstance(ctx context.Context, address string, port int) (up, cancelled bool) {
135143
instanceUp := make(chan bool)
136144
go func() {
137145
client := &http.Client{Timeout: time.Second * 10}
146+
scheme := "http"
147+
if s.IsSecure() {
148+
scheme = "https"
149+
client.Transport = &http.Transport{
150+
TLSClientConfig: &tls.Config{
151+
InsecureSkipVerify: true,
152+
},
153+
}
154+
}
138155
makeRequest := func() error {
139156
addr := net.JoinHostPort(address, strconv.Itoa(port))
140-
url := fmt.Sprintf("http://%s/_api/version", addr)
157+
url := fmt.Sprintf("%s://%s/_api/version", scheme, addr)
141158
req, err := http.NewRequest("GET", url, nil)
142159
if err != nil {
143160
return maskAny(err)
@@ -175,6 +192,10 @@ func (s *Service) testInstance(ctx context.Context, address string, port int) (u
175192
func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress string, myPort string, mode string) (args []string, configVolumes []Volume) {
176193
hostConfFileName := filepath.Join(myHostDir, "arangod.conf")
177194
containerConfFileName := filepath.Join(myContainerDir, "arangod.conf")
195+
scheme := "tcp"
196+
if s.IsSecure() {
197+
scheme = "ssl"
198+
}
178199

179200
if runtime.GOOS != "linux" {
180201
configVolumes = append(configVolumes, Volume{
@@ -202,7 +223,7 @@ func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress strin
202223
serverSection := &configSection{
203224
Name: "server",
204225
Settings: map[string]string{
205-
"endpoint": fmt.Sprintf("tcp://[::]:%s", myPort),
226+
"endpoint": fmt.Sprintf("%s://[::]:%s", scheme, myPort),
206227
"threads": threads,
207228
"authentication": "false",
208229
},
@@ -226,6 +247,18 @@ func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress strin
226247
},
227248
},
228249
}
250+
if s.IsSecure() {
251+
sslSection := &configSection{
252+
Name: "ssl",
253+
Settings: map[string]string{
254+
"keyfile": s.SslKeyFile,
255+
},
256+
}
257+
if s.SslCAFile != "" {
258+
sslSection.Settings["cafile"] = s.SslCAFile
259+
}
260+
config = append(config, sslSection)
261+
}
229262

230263
out, e := os.Create(hostConfFileName)
231264
if e != nil {
@@ -255,7 +288,7 @@ func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress strin
255288
if s.ServerThreads != 0 {
256289
args = append(args, "--server.threads", strconv.Itoa(s.ServerThreads))
257290
}
258-
myTCPURL := "tcp://" + net.JoinHostPort(myAddress, myPort)
291+
myTCPURL := scheme + "://" + net.JoinHostPort(myAddress, myPort)
259292
switch mode {
260293
case "agent":
261294
args = append(args,
@@ -270,7 +303,7 @@ func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress strin
270303
if p.HasAgent && p.ID != s.ID {
271304
args = append(args,
272305
"--agency.endpoint",
273-
fmt.Sprintf("tcp://%s", net.JoinHostPort(p.Address, strconv.Itoa(s.MasterPort+p.PortOffset+portOffsetAgent))),
306+
fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(p.Address, strconv.Itoa(s.MasterPort+p.PortOffset+portOffsetAgent))),
274307
)
275308
}
276309
}
@@ -296,7 +329,7 @@ func (s *Service) makeBaseArgs(myHostDir, myContainerDir string, myAddress strin
296329
p := s.myPeers.Peers[i]
297330
args = append(args,
298331
"--cluster.agency-endpoint",
299-
fmt.Sprintf("tcp://%s", net.JoinHostPort(p.Address, strconv.Itoa(s.MasterPort+p.PortOffset+portOffsetAgent))),
332+
fmt.Sprintf("%s://%s", scheme, net.JoinHostPort(p.Address, strconv.Itoa(s.MasterPort+p.PortOffset+portOffsetAgent))),
300333
)
301334
}
302335
}

service/certificate.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package service
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/rsa"
6+
"crypto/x509"
7+
"crypto/x509/pkix"
8+
"encoding/pem"
9+
"fmt"
10+
"io/ioutil"
11+
"math/big"
12+
"net"
13+
"time"
14+
)
15+
16+
// CreateCertificateOptions configures how to create a certificate.
17+
type CreateCertificateOptions struct {
18+
Hosts []string // Host names and/or IP addresses
19+
ValidFor time.Duration
20+
RSABits int
21+
Organization string
22+
}
23+
24+
const (
25+
defaultValidFor = time.Hour * 24 * 365 // 1year
26+
)
27+
28+
func publicKey(priv interface{}) interface{} {
29+
switch k := priv.(type) {
30+
case *rsa.PrivateKey:
31+
return &k.PublicKey
32+
default:
33+
return nil
34+
}
35+
}
36+
37+
func pemBlockForKey(priv interface{}) *pem.Block {
38+
switch k := priv.(type) {
39+
case *rsa.PrivateKey:
40+
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
41+
default:
42+
return nil
43+
}
44+
}
45+
46+
// CreateCertificate creates a self-signed certificate according to the given configuration.
47+
// The resulting certificate + private key will be written into a single file in the given folder.
48+
// The path of that single file is returned.
49+
func CreateCertificate(options CreateCertificateOptions, folder string) (string, error) {
50+
priv, err := rsa.GenerateKey(rand.Reader, options.RSABits)
51+
if err != nil {
52+
return "", maskAny(err)
53+
}
54+
55+
notBefore := time.Now()
56+
if options.ValidFor == 0 {
57+
options.ValidFor = defaultValidFor
58+
}
59+
notAfter := notBefore.Add(options.ValidFor)
60+
61+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
62+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
63+
if err != nil {
64+
return "", maskAny(fmt.Errorf("failed to generate serial number: %v", err))
65+
}
66+
67+
template := x509.Certificate{
68+
SerialNumber: serialNumber,
69+
Subject: pkix.Name{
70+
Organization: []string{options.Organization},
71+
},
72+
NotBefore: notBefore,
73+
NotAfter: notAfter,
74+
75+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
76+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
77+
BasicConstraintsValid: true,
78+
}
79+
80+
for _, h := range options.Hosts {
81+
if ip := net.ParseIP(h); ip != nil {
82+
template.IPAddresses = append(template.IPAddresses, ip)
83+
} else {
84+
template.DNSNames = append(template.DNSNames, h)
85+
}
86+
}
87+
88+
// Create the certificate
89+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
90+
if err != nil {
91+
return "", maskAny(fmt.Errorf("Failed to create certificate: %v", err))
92+
}
93+
94+
// Write the certificate to disk
95+
f, err := ioutil.TempFile(folder, "key-")
96+
if err != nil {
97+
return "", maskAny(err)
98+
}
99+
defer f.Close()
100+
// Public key
101+
pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
102+
// Private key
103+
pem.Encode(f, pemBlockForKey(priv))
104+
105+
return f.Name(), nil
106+
}

service/peers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Peer struct {
1414
PortOffset int // Offset to add to base ports for the various servers (agent, coordinator, dbserver)
1515
DataDir string // Directory holding my data
1616
HasAgent bool // If set, this peer is running an agent
17+
IsSecure bool // If set, servers started by this peer are using an SSL connection
1718
}
1819

1920
// CreateStarterURL creates a URL to the relative path to the starter on this peer.

0 commit comments

Comments
 (0)