Skip to content

Commit

Permalink
Cluster secret: Docs, error handling, internal key mgmt.
Browse files Browse the repository at this point in the history
  • Loading branch information
dgrisham committed Jul 13, 2017
1 parent 9833590 commit 7c3ab29
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 24 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ This will install `ipfs-cluster-service` and `ipfs-cluster-ctl` in your `$GOPATH
**`ipfs-cluster-service`** runs an ipfs-cluster peer:

* Initialize with `ipfs-cluster-service init`
* You will be asked to enter a cluster secret. For more on this, see [the Cluster secret section of the `ipfs-cluster-service` * README](ipfs-cluster-service/dist/README.md#cluster-secret).
* Run with `ipfs-cluster-service`. Check `--help` for options

For more information see the [`ipfs-cluster-service` README](ipfs-cluster-service/dist/README.md). Also, read [A guide to running IPFS Cluster](docs/ipfs-cluster-guide.md) for full a full overview of how cluster works.
Expand Down
47 changes: 33 additions & 14 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
crand "crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
Expand Down Expand Up @@ -37,8 +38,8 @@ type Config struct {
PrivateKey crypto.PrivKey

// Cluster secret for private network. Peers will be in the same cluster if and
// only if they have the same ClusterSecret. If this value is empty, then the
// cluster will be on a unprotected public network (accessible by anyone).
// only if they have the same ClusterSecret. The cluster secret must be exactly
// 64 characters and contain only hexadecimal characters (`[0-9a-f]`).
ClusterSecret []byte

// ClusterPeers is the list of peers in the Cluster. They are used
Expand Down Expand Up @@ -111,8 +112,8 @@ type JSONConfig struct {
PrivateKey string `json:"private_key"`

// Cluster secret for private network. Peers will be in the same cluster if and
// only if they have the same ClusterSecret. If this value is empty, then the
// cluster will be on a unprotected public network (accessible by anyone).
// only if they have the same ClusterSecret. The cluster secret must be exactly
// 64 characters and contain only hexadecimal characters (`[0-9a-f]`).
ClusterSecret string `json:"cluster_secret"`

// ClusterPeers is the list of peers' multiaddresses in the Cluster.
Expand Down Expand Up @@ -206,7 +207,7 @@ func (cfg *Config) ToJSONConfig() (j *JSONConfig, err error) {
j = &JSONConfig{
ID: cfg.ID.Pretty(),
PrivateKey: pKey,
ClusterSecret: string(cfg.ClusterSecret),
ClusterSecret: EncodeClusterSecret(cfg.ClusterSecret),
ClusterPeers: clusterPeers,
Bootstrap: bootstrap,
LeaveOnShutdown: cfg.LeaveOnShutdown,
Expand Down Expand Up @@ -243,6 +244,11 @@ func (jcfg *JSONConfig) ToConfig() (c *Config, err error) {
err = fmt.Errorf("error parsing private_key ID: %s", err)
return
}
clusterSecret, err := DecodeClusterSecret(jcfg.ClusterSecret)
if err != nil {
err = fmt.Errorf("error loading cluster secret from config: ", err)
return nil, err
}

clusterPeers := make([]ma.Multiaddr, len(jcfg.ClusterPeers))
for i := 0; i < len(jcfg.ClusterPeers); i++ {
Expand Down Expand Up @@ -312,7 +318,7 @@ func (jcfg *JSONConfig) ToConfig() (c *Config, err error) {
c = &Config{
ID: id,
PrivateKey: pKey,
ClusterSecret: []byte(jcfg.ClusterSecret),
ClusterSecret: clusterSecret,
ClusterPeers: clusterPeers,
Bootstrap: bootstrap,
LeaveOnShutdown: jcfg.LeaveOnShutdown,
Expand Down Expand Up @@ -410,26 +416,39 @@ func (cfg *Config) unshadow() {
}

func generateClusterSecret() ([]byte, error) {
encodedSecretLength := 64
secret := make([]byte, base64.StdEncoding.DecodedLen(encodedSecretLength))
_, err := crand.Read(secret)
secretBytes := make([]byte, 32)
_, err := crand.Read(secretBytes)
if err != nil {
return nil, fmt.Errorf("Error reading from rand: %v", err)
}
encodedSecret := make([]byte, encodedSecretLength)
base64.StdEncoding.Encode(encodedSecret, secret)
return encodedSecret, nil
return secretBytes, nil
}

func clusterSecretToKey(secret []byte) (string, error) {
var key bytes.Buffer
key.WriteString("/key/swarm/psk/1.0.0/\n")
key.WriteString("/base64/\n")
key.Write(secret)
key.WriteString("/base16/\n")
key.WriteString(EncodeClusterSecret(secret))

return key.String(), nil
}

func DecodeClusterSecret(hexSecret string) ([]byte, error) {
secret, err := hex.DecodeString(hexSecret)
if err != nil {
return nil, err
}
secretLen := len(secret)
if secretLen != 32 {
return nil, fmt.Errorf("Input secret is %d bytes, cluster secret should be 32.", secretLen)
}
return secret, nil
}

func EncodeClusterSecret(secretBytes []byte) string {
return hex.EncodeToString(secretBytes)
}

// NewDefaultConfig returns a default configuration object with a randomly
// generated ID and private key.
func NewDefaultConfig() (*Config, error) {
Expand Down
4 changes: 2 additions & 2 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package ipfscluster

import "testing"

var testingClusterSecret = []byte("2951539a3737c06a5aee55834c27145ca1783bdc7daeaa92f9712b3ff6e9fa25")
var testingClusterSecret, _ = DecodeClusterSecret("2588b80d5cb05374fa142aed6cbb047d1f4ef8ef15e37eba68c65b9d30df67ed")

func testingConfig() *Config {
jcfg := &JSONConfig{
ID: "QmUfSFm12eYCaRdypg48m8RqkXfLW7A2ZeGZb2skeHHDGA",
PrivateKey: "CAASqAkwggSkAgEAAoIBAQDpT16IRF6bb9tHsCbQ7M+nb2aI8sz8xyt8PoAWM42ki+SNoESIxKb4UhFxixKvtEdGxNE6aUUVc8kFk6wTStJ/X3IGiMetwkXiFiUxabUF/8A6SyvnSVDm+wFuavugpVrZikjLcfrf2xOVgnG3deQQvd/qbAv14jTwMFl+T+8d/cXBo8Mn/leLZCQun/EJEnkXP5MjgNI8XcWUE4NnH3E0ESSm6Pkm8MhMDZ2fmzNgqEyJ0GVinNgSml3Pyha3PBSj5LRczLip/ie4QkKx5OHvX2L3sNv/JIUHse5HSbjZ1c/4oGCYMVTYCykWiczrxBUOlcr8RwnZLOm4n2bCt5ZhAgMBAAECggEAVkePwfzmr7zR7tTpxeGNeXHtDUAdJm3RWwUSASPXgb5qKyXVsm5nAPX4lXDE3E1i/nzSkzNS5PgIoxNVU10cMxZs6JW0okFx7oYaAwgAddN6lxQtjD7EuGaixN6zZ1k/G6vT98iS6i3uNCAlRZ9HVBmjsOF8GtYolZqLvfZ5izEVFlLVq/BCs7Y5OrDrbGmn3XupfitVWYExV0BrHpobDjsx2fYdTZkmPpSSvXNcm4Iq2AXVQzoqAfGo7+qsuLCZtVlyTfVKQjMvE2ffzN1dQunxixOvev/fz4WSjGnRpC6QLn6Oqps9+VxQKqKuXXqUJC+U45DuvA94Of9MvZfAAQKBgQD7xmXueXRBMr2+0WftybAV024ap0cXFrCAu+KWC1SUddCfkiV7e5w+kRJx6RH1cg4cyyCL8yhHZ99Z5V0Mxa/b/usuHMadXPyX5szVI7dOGgIC9q8IijN7B7GMFAXc8+qC7kivehJzjQghpRRAqvRzjDls4gmbNPhbH1jUiU124QKBgQDtOaW5/fOEtOq0yWbDLkLdjImct6oKMLhENL6yeIKjMYgifzHb2adk7rWG3qcMrdgaFtDVfqv8UmMEkzk7bSkovMVj3SkLzMz84ii1SkSfyaCXgt/UOzDkqAUYB0cXMppYA7jxHa2OY8oEHdBgmyJXdLdzJxCp851AoTlRUSePgQKBgQCQgKgUHOUaXnMEx88sbOuBO14gMg3dNIqM+Ejt8QbURmI8k3arzqA4UK8Tbb9+7b0nzXWanS5q/TT1tWyYXgW28DIuvxlHTA01aaP6WItmagrphIelERzG6f1+9ib/T4czKmvROvDIHROjq8lZ7ERs5Pg4g+sbh2VbdzxWj49EQQKBgFEna36ZVfmMOs7mJ3WWGeHY9ira2hzqVd9fe+1qNKbHhx7mDJR9fTqWPxuIh/Vac5dZPtAKqaOEO8OQ6f9edLou+ggT3LrgsS/B3tNGOPvA6mNqrk/Yf/15TWTO+I8DDLIXc+lokbsogC+wU1z5NWJd13RZZOX/JUi63vTmonYBAoGBAIpglLCH2sPXfmguO6p8QcQcv4RjAU1c0GP4P5PNN3Wzo0ItydVd2LHJb6MdmL6ypeiwNklzPFwTeRlKTPmVxJ+QPg1ct/3tAURN/D40GYw9ojDhqmdSl4HW4d6gHS2lYzSFeU5jkG49y5nirOOoEgHy95wghkh6BfpwHujYJGw4",
ClusterSecret: string(testingClusterSecret),
ClusterSecret: EncodeClusterSecret(testingClusterSecret),

ClusterListenMultiaddress: "/ip4/127.0.0.1/tcp/10000",
APIListenMultiaddress: "/ip4/127.0.0.1/tcp/10002",
Expand Down
3 changes: 2 additions & 1 deletion docs/HOWTO_build_and_update_a_cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

This step creates a single-node IPFS Cluster.

First initialize the configuration:
First initialize the configuration (see [the Cluster secret section of the `ipfs-cluster-service` README](ipfs-cluster-service/dist/README.md#cluster-secret) for more info on entering a cluster secret):

```
node0 $ ipfs-cluster-service init
Enter cluster secret (to automatically generate, rerun with --gen-secret): <enter-your-secret>
ipfs-cluster-service configuration written to /home/user/.ipfs-cluster/service.json
```

Expand Down
21 changes: 21 additions & 0 deletions ipfs-cluster-service/dist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ Before running `ipfs-cluster-service` for the first time, initialize a configura
$ ipfs-cluster-service init
```

### Cluster secret

As long as the `CLUSTER_SECRET` environment variable is not set, you will be
prompted to enter a `cluster_secret` value for your configuration. Two peers
will be in the same cluster if and only if they share the same `cluster_secret`
value. When initializing a cluster service, the following conditions are checked
(in the order shown) and determine how `cluster_secret` is set:

1. If the `--gen-secret` flag is passed to `ipfs-cluster-service init`, then a
`cluster_secret` value will be automatically generated.
2. If the `CLUSTER_SECRET` environment variable is set in your current shell
instance, its value will be read and used as the `cluster_service` value.
3. If neither of the above conditions were ture, then you will be prompted to
enter a `cluster_secret` during the initialization.

The `cluster_secret` must be a 64-character string with only hexadecimal
characters (`[0-9a-f]`).

TODO: Explain adding peers/sharing cluster secret once that functionality is
implemented.

### Configuration

Expand All @@ -33,6 +53,7 @@ You can add the multiaddresses for the other cluster peers the `bootstrap` varia
{
"id": "QmXMhZ53zAoes8TYbKGn3rnm5nfWs5Wdu41Fhhfw9XmM5A",
"private_key": "<redacted>",
"cluster_secret": "<redacted>",
"cluster_peers": [],
"bootstrap": [],
"leave_on_shutdown": false,
Expand Down
11 changes: 6 additions & 5 deletions ipfs-cluster-service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ func main() {
},
Action: func(c *cli.Context) error {
initConfig(c.GlobalBool("force"), c.Bool("gen-secret"),
[]byte(c.GlobalString("env-cluster-secret")))
c.GlobalString("env-cluster-secret"))
return nil
},
},
Expand Down Expand Up @@ -238,7 +238,7 @@ func main() {

func run(c *cli.Context) error {
if c.Bool("init") {
initConfig(c.Bool("force"), false, nil)
initConfig(c.Bool("force"), false, "")
return nil
}

Expand Down Expand Up @@ -350,7 +350,7 @@ func setupDebug() {
//SetFacilityLogLevel("libp2p-raft", l)
}

func initConfig(force bool, generateSecret bool, envSecret []byte) {
func initConfig(force bool, generateSecret bool, envSecret string) {
if _, err := os.Stat(configPath); err == nil && !force {
err := fmt.Errorf("%s exists. Try running with -f", configPath)
checkErr("", err)
Expand All @@ -363,11 +363,12 @@ func initConfig(force bool, generateSecret bool, envSecret []byte) {
if len(envSecret) != 0 {
// read cluster secret from env variable
fmt.Println("Reading cluster secret from CLUSTER_SECRET environment variable.")
cfg.ClusterSecret = []byte(envSecret)
cfg.ClusterSecret, err = ipfscluster.DecodeClusterSecret(envSecret)
} else {
// get cluster secret from user
cfg.ClusterSecret = []byte(promptUser("Enter cluster secret (to automatically generate, rerun with --gen-secret): "))
cfg.ClusterSecret, err = ipfscluster.DecodeClusterSecret(promptUser("Enter cluster secret (to automatically generate, rerun with --gen-secret): "))
}
checkErr("parsing cluster secret", err)
}

cfg.ConsensusDataFolder = dataPath
Expand Down
8 changes: 6 additions & 2 deletions pnet_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ipfscluster

import "testing"
import "fmt"

func TestSimplePNet(t *testing.T) {
clusters, mocks := peerManagerClusters(t)
Expand All @@ -27,10 +28,13 @@ func TestSimplePNet(t *testing.T) {
func TestClusterSecretRequired(t *testing.T) {
cl1Secret, err := generateClusterSecret()
if err != nil {
t.Fatal("Unable to generate cluster secret")
t.Fatal("Unable to generate cluster secret.")
}
cl1, _ := createOnePeerCluster(t, 1, cl1Secret)
fmt.Println("HERE 1")
cl2, _ := createOnePeerCluster(t, 2, testingClusterSecret)
fmt.Println("HERE 2")
cl1, _ := createOnePeerCluster(t, 1, cl1Secret)
fmt.Println("HERE 3")
defer cleanRaft()
defer cl1.Shutdown()
defer cl2.Shutdown()
Expand Down

0 comments on commit 7c3ab29

Please sign in to comment.