diff --git a/README.md b/README.md index 2bcd5bd..58aca08 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ my.instance.dir.org/ ├── internal_auth_password ├── postgres_password ├── superadmin + ├── vote_key ├── cert_crt # (if HTTPS enabled) └── cert_key ``` diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 0ee0755..e3f6de2 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -28,6 +28,9 @@ const ( // PgPasswordFile contains the PostgreSQL database password PgPasswordFile string = "postgres_password" + // VoteKeyFile contains the vote key secret + VoteKeyFile string = "vote_key" + // AuthTokenKey contains the authentication token secret AuthTokenKey string = "auth_token_key" @@ -82,6 +85,9 @@ const ( // DefaultPostgresPasswordLength is the default length for database passwords DefaultPostgresPasswordLength int = 40 + // DefaultVoteKeyLength is the default length for vote key secret + DefaultVoteKeyLength int = 40 + // DefaultSecretBytesLength is the number of random bytes used for base64-encoded secrets. // These 32 bytes produce a 44-character base64 string used for: // - auth_token_key diff --git a/internal/grpc/server/create.go b/internal/grpc/server/create.go index c155d76..a176e00 100644 --- a/internal/grpc/server/create.go +++ b/internal/grpc/server/create.go @@ -8,7 +8,7 @@ import ( ) func (s *OsmanageServiceServer) CreateInstance(ctx context.Context, req *pb.CreateInstanceRequest) (*pb.CreateInstanceResponse, error) { - if err := create.CreateInstance(req.InstanceDir, req.DbPassword, req.SuperadminPassword); err != nil { + if err := create.CreateInstance(req.InstanceDir, req.DbPassword, req.SuperadminPassword, req.VoteKey); err != nil { return &pb.CreateInstanceResponse{Success: false, Error: err.Error()}, nil } return &pb.CreateInstanceResponse{Success: true}, nil diff --git a/internal/instance/create/create.go b/internal/instance/create/create.go index 8383473..e384365 100644 --- a/internal/instance/create/create.go +++ b/internal/instance/create/create.go @@ -21,12 +21,13 @@ This command: 2. Sets all secret files to 600 permissions 3. Writes the database password to postgres_password 4. Writes the superadmin password to superadmin +5. Writes the vote key to vote_key The secrets directory must already exist (created by 'setup' command). Examples: - osmanage create ./my.instance.dir.org --db-password "mydbpass" --superadmin-password "myadminpass" - osmanage create ./my.instance.dir.org --db-password "$(cat db.txt)" --superadmin-password "$(cat admin.txt)"` + osmanage create ./my.instance.dir.org --db-password "mydbpass" --superadmin-password "myadminpass" --vote-key "myvotekey" + osmanage create ./my.instance.dir.org --db-password "$(cat db.txt)" --superadmin-password "$(cat admin.txt)" --vote-key "$(cat vote_key.txt)"` ) func Cmd() *cobra.Command { @@ -39,16 +40,18 @@ func Cmd() *cobra.Command { dbPassword := cmd.Flags().String("db-password", "", "PostgreSQL database password (required)") superadminPassword := cmd.Flags().String("superadmin-password", "", "Superadmin password (required)") + voteKey := cmd.Flags().String("vote-key", "", "Vote Key (required)") _ = cmd.MarkFlagRequired("db-password") _ = cmd.MarkFlagRequired("superadmin-password") + _ = cmd.MarkFlagRequired("vote-key") cmd.RunE = func(cmd *cobra.Command, args []string) error { logger.Info("=== K8S CREATE INSTANCE ===") instanceDir := args[0] logger.Debug("Instance directory: %s", instanceDir) - if err := CreateInstance(instanceDir, *dbPassword, *superadminPassword); err != nil { + if err := CreateInstance(instanceDir, *dbPassword, *superadminPassword, *voteKey); err != nil { return fmt.Errorf("creating instance: %w", err) } @@ -60,13 +63,16 @@ func Cmd() *cobra.Command { } // CreateInstance sets up the secrets directory with the provided passwords -func CreateInstance(instanceDir, dbPassword, superadminPassword string) error { +func CreateInstance(instanceDir, dbPassword, superadminPassword, voteKey string) error { if strings.TrimSpace(dbPassword) == "" { return fmt.Errorf("db_password cannot be empty") } if strings.TrimSpace(superadminPassword) == "" { return fmt.Errorf("superadmin_password cannot be empty") } + if strings.TrimSpace(voteKey) == "" { + return fmt.Errorf("vote_key cannot be empty") + } secretsDir := filepath.Join(instanceDir, constants.SecretsDirName) @@ -93,6 +99,12 @@ func CreateInstance(instanceDir, dbPassword, superadminPassword string) error { return fmt.Errorf("writing superadmin password: %w", err) } + voteKeyPath := filepath.Join(secretsDir, constants.VoteKeyFile) + logger.Debug("Writing vote key to: %s", voteKeyPath) + if err := os.WriteFile(voteKeyPath, []byte(voteKey), constants.SecretFilePerm); err != nil { + return fmt.Errorf("writing vote key: %w", err) + } + logger.Info("Passwords configured successfully") return nil } diff --git a/internal/instance/create/create_test.go b/internal/instance/create/create_test.go index 0ab77b2..45a9774 100644 --- a/internal/instance/create/create_test.go +++ b/internal/instance/create/create_test.go @@ -143,6 +143,7 @@ func TestCreateInstance(t *testing.T) { existingSecrets := map[string]string{ constants.PgPasswordFile: "old-db-password", constants.AdminSecretsFile: "old-admin-password", + constants.VoteKeyFile: "old-vote-key", constants.InternalAuthPassword: "some-auth-key", } @@ -156,8 +157,9 @@ func TestCreateInstance(t *testing.T) { // Run createInstance dbPassword := "new-database-password" superadminPassword := "new-superadmin-password" + voteKey := "new-vote-key" - err = CreateInstance(tmpDir, dbPassword, superadminPassword) + err = CreateInstance(tmpDir, dbPassword, superadminPassword, voteKey) if err != nil { t.Fatalf("createInstance failed: %v", err) } @@ -180,6 +182,15 @@ func TestCreateInstance(t *testing.T) { t.Errorf("superadmin = %q, want %q", string(adminContent), superadminPassword) } + // Verify vote_key was overwritten + voteKeyContent, err := os.ReadFile(filepath.Join(secretsDir, constants.VoteKeyFile)) + if err != nil { + t.Fatalf("Failed to read vote_key: %v", err) + } + if string(voteKeyContent) != voteKey { + t.Errorf("vote_key = %q, want %q", string(voteKeyContent), voteKey) + } + // Verify other secrets were not touched authContent, err := os.ReadFile(filepath.Join(secretsDir, constants.InternalAuthPassword)) if err != nil { @@ -234,7 +245,7 @@ func TestCreateInstance_SecretsDirectoryNotExist(t *testing.T) { }) // Don't create secrets directory - should fail - err = CreateInstance(tmpDir, "password", "admin") + err = CreateInstance(tmpDir, "password", "admin", "key") if err == nil { t.Error("Expected error when secrets directory doesn't exist, got nil") } diff --git a/internal/instance/setup/setup.go b/internal/instance/setup/setup.go index e32998a..30ba65d 100644 --- a/internal/instance/setup/setup.go +++ b/internal/instance/setup/setup.go @@ -51,6 +51,7 @@ var defaultSecrets = []SecretSpec{ {constants.AuthCookieKey, randomSecret}, {constants.InternalAuthPassword, randomSecret}, {constants.PgPasswordFile, func() ([]byte, error) { return randomString(constants.DefaultPostgresPasswordLength) }}, + {constants.VoteKeyFile, func() ([]byte, error) { return randomString(constants.DefaultVoteKeyLength) }}, {constants.AdminSecretsFile, func() ([]byte, error) { return randomString(constants.DefaultSuperadminPasswordLength) }}, } diff --git a/internal/instance/setup/setup_test.go b/internal/instance/setup/setup_test.go index f562087..72ccd81 100644 --- a/internal/instance/setup/setup_test.go +++ b/internal/instance/setup/setup_test.go @@ -246,6 +246,7 @@ func TestDefaultSecrets(t *testing.T) { constants.AuthCookieKey, constants.InternalAuthPassword, constants.PgPasswordFile, + constants.VoteKeyFile, constants.AdminSecretsFile, } @@ -285,6 +286,19 @@ func TestDefaultSecrets(t *testing.T) { } } + // Test vote_key generates proper string + for _, spec := range defaultSecrets { + if spec.Name == constants.VoteKeyFile { + pwd, err := spec.Generator() + if err != nil { + t.Errorf("vote_key generator error = %v", err) + } + if len(pwd) != constants.DefaultVoteKeyLength { + t.Errorf("Expected length %d, got %d", constants.DefaultVoteKeyLength, len(pwd)) + } + } + } + // Test that base64-encoded secrets still work as expected for _, spec := range defaultSecrets { if spec.Name == constants.AuthTokenKey || spec.Name == constants.AuthCookieKey || spec.Name == constants.InternalAuthPassword { @@ -384,6 +398,13 @@ tag: {{ .defaults.tag }} if len(postgresPwd) != constants.DefaultPostgresPasswordLength { t.Errorf("Expected postgres password length %d, got %d", constants.DefaultPostgresPasswordLength, len(postgresPwd)) } + + // 6. Check vote_key has correct length + voteKeyPath := filepath.Join(secretsDir, constants.VoteKeyFile) + voteKey, _ := os.ReadFile(voteKeyPath) + if len(voteKey) != constants.DefaultVoteKeyLength { + t.Errorf("Expected vote_key length %d, got %d", constants.DefaultVoteKeyLength, len(voteKey)) + } }) t.Run("template processing with camelCase", func(t *testing.T) { diff --git a/proto/osmanage.proto b/proto/osmanage.proto index 4df2b61..68e803a 100644 --- a/proto/osmanage.proto +++ b/proto/osmanage.proto @@ -69,6 +69,7 @@ message CreateInstanceRequest { string instance_dir = 1; string db_password = 2; string superadmin_password = 3; + string vote_key = 4; } message CreateInstanceResponse { diff --git a/proto/osmanage/osmanage.pb.go b/proto/osmanage/osmanage.pb.go index 6c3db86..4d620ff 100644 --- a/proto/osmanage/osmanage.pb.go +++ b/proto/osmanage/osmanage.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v4.25.1 +// protoc v3.21.12 // source: proto/osmanage.proto package osmanage @@ -250,6 +250,7 @@ type CreateInstanceRequest struct { InstanceDir string `protobuf:"bytes,1,opt,name=instance_dir,json=instanceDir,proto3" json:"instance_dir,omitempty"` DbPassword string `protobuf:"bytes,2,opt,name=db_password,json=dbPassword,proto3" json:"db_password,omitempty"` SuperadminPassword string `protobuf:"bytes,3,opt,name=superadmin_password,json=superadminPassword,proto3" json:"superadmin_password,omitempty"` + VoteKey string `protobuf:"bytes,4,opt,name=vote_key,json=voteKey,proto3" json:"vote_key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -305,6 +306,13 @@ func (x *CreateInstanceRequest) GetSuperadminPassword() string { return "" } +func (x *CreateInstanceRequest) GetVoteKey() string { + if x != nil { + return x.VoteKey + } + return "" +} + type CreateInstanceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` @@ -2657,12 +2665,13 @@ const file_proto_osmanage_proto_rawDesc = "" + "\x05clean\x18\x05 \x01(\bR\x05clean\"H\n" + "\x16InstanceConfigResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" + - "\x05error\x18\x02 \x01(\tR\x05error\"\x8c\x01\n" + + "\x05error\x18\x02 \x01(\tR\x05error\"\xa7\x01\n" + "\x15CreateInstanceRequest\x12!\n" + "\finstance_dir\x18\x01 \x01(\tR\vinstanceDir\x12\x1f\n" + "\vdb_password\x18\x02 \x01(\tR\n" + "dbPassword\x12/\n" + - "\x13superadmin_password\x18\x03 \x01(\tR\x12superadminPassword\"H\n" + + "\x13superadmin_password\x18\x03 \x01(\tR\x12superadminPassword\x12\x19\n" + + "\bvote_key\x18\x04 \x01(\tR\avoteKey\"H\n" + "\x16CreateInstanceResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" + "\x05error\x18\x02 \x01(\tR\x05error\"P\n" + diff --git a/proto/osmanage/osmanage_grpc.pb.go b/proto/osmanage/osmanage_grpc.pb.go index a74d3fd..48a1fed 100644 --- a/proto/osmanage/osmanage_grpc.pb.go +++ b/proto/osmanage/osmanage_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 -// - protoc v4.25.1 +// - protoc v3.21.12 // source: proto/osmanage.proto package osmanage