This commit is contained in:
2023-04-03 10:11:56 +10:00
parent 1acf4c48d7
commit 75468ee8f3
4 changed files with 79 additions and 6 deletions

View File

@@ -1,7 +1,35 @@
# CC Secrets
## Overview
Design concepts at https://wiki.coadcorp.com/doc/secrets-management-idea-VGJMey7Wnd
Provide REST API for CRUD to store and retrieve user/password data for logging into devices. Only password is encrypted, via AES256 GCM. Values stored in sqlite database.
Provide REST API for CRUD to store and retrieve user/password data for logging into devices. Only password is encrypted, via AES256 GCM. Values stored in sqlite database.
Requires JWT token to store/retrieve passwords.
This isn't super secure, probably not even as secure as Hashicorp Vault running in dev mode.
This isn't super secure, probably not even as secure as Hashicorp Vault running in dev mode.
## Installation
1. Copy binary to chosen location, eg /srv/ccsecrets
2. Create .env file in same directory as binary, populate as per Configuration section below
3. Create systemd service definition
4. Start service
## Configuration
|Environment Variable Name| Description | Example | Default |
|--|--|--|--|
| LOG_FILE | Specify the name/path of file to write log messages to | /var/log/ccsecrets.log | ./ccsecrets.log
| BIND_IP | Specify the local IP address to bind to. | 127.0.0.1 | Primary IPv4 address |
| BIND_PORT | Specify the TCP/IP port to bind to. | 443 | 8443 |
| TLS_KEY_FILE | Specify the filename of the TLS certificate private key (must be unencrypted) in PEM format | key.pem | privkey.pem |
| TLS_CERT_FILE | Specify the filename of the TLS certificate file in PEM format | cert.pem | cert.pem |
| TOKEN_HOUR_LIFESPAN | Number of hours that the JWT token returned at login is valid | 12 | No default specified, must define this value |
| API_SECRET | Secret to use when generating JWT token | 3c55990bd479322e2053db3a8 | No default specified, must define this value |
| INITIAL_PASSWORD | Password to set for builtin Administrator account created when first started, can remove this value after first start. Can specify in plaintext or bcrypt hash | $2a$10$s39a82wrRAdOJVZEkkrSReVnXprz5mxU30ZBO.dHPYTncQCsUD9ce | password
| SECRETS_KEY | Key to use for AES256 GCM encryption. Must be exactly 32 bytes | AES256Key-32Characters1234567890 | No default specified, must define this value |
## Systemd script
## API

View File

@@ -83,7 +83,17 @@ func UpdateSecret(c *gin.Context) {
fmt.Printf("UpdateSecret received JSON input '%v'\n", input)
// TODO - verify that the user role is not readonly
// Get the user and role id of the requestor
u, err := models.GetUserRoleFromToken(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Verify that the user role is not readonly
if u.ReadOnly {
c.JSON(http.StatusForbidden, gin.H{"error": "user role does not permit updates"})
return
}
// Populate fields
s := models.Secret{}

18
main.go
View File

@@ -90,10 +90,22 @@ func main() {
bindAddress := fmt.Sprint(bindIP, ":", bindPort)
fmt.Printf("Will listen on address 'https://%s'\n", bindAddress)
// Generate certificate if required
tlsCertFilename := utils.GetFilePath(os.Getenv("TLS_CERT_FILE"))
tlsKeyFilename := utils.GetFilePath(os.Getenv("TLS_KEY_FILE"))
// Get file names for TLS cert/key
tlsCertFilename := os.Getenv("TLS_CERT_FILE")
if tlsCertFilename != "" {
tlsCertFilename = utils.GetFilePath(tlsCertFilename)
} else {
tlsCertFilename = "./cert.pem"
}
tlsKeyFilename := os.Getenv("TLS_KEY_FILE")
if tlsKeyFilename != "" {
tlsKeyFilename = utils.GetFilePath(tlsKeyFilename)
} else {
tlsKeyFilename = "./privkey.pem"
}
// Generate certificate if required
if !(utils.FileExists(tlsCertFilename) && utils.FileExists(tlsKeyFilename)) {
fmt.Printf("Specified TLS certificate (%s) or private key (%s) do not exist.\n", tlsCertFilename, tlsKeyFilename)
utils.GenerateCerts(tlsCertFilename, tlsKeyFilename)

View File

@@ -4,7 +4,9 @@ import (
"ccsecrets/utils/token"
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
@@ -128,7 +130,28 @@ func GetUserRoleByID(uid uint) (UserRole, error) {
}
return ur, nil
}
func GetUserRoleFromToken(c *gin.Context) (UserRole, error) {
var ur UserRole
user_id, err := token.ExtractTokenID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return ur, err
}
// Query database for matching user object
fmt.Printf("GetUserRoleFromToken querying for userid '%d'\n", user_id)
err = db.QueryRowx("SELECT users.UserId, users.RoleId, users.UserName, users.Password, roles.RoleName, roles.ReadOnly, roles.Admin FROM users INNER JOIN roles ON users.RoleId = roles.RoleId WHERE users.UserId=?", user_id).StructScan(&ur)
if err != nil {
fmt.Printf("GetUserRoleFromToken received error when querying database : '%s'\n", err)
return ur, errors.New("GetUserRoleFromToken user not found")
}
return ur, nil
}
/*