From 75468ee8f3cbd4d432230c6de5b9ce3b068e0a2b Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Mon, 3 Apr 2023 10:11:56 +1000 Subject: [PATCH] updates --- README.md | 32 ++++++++++++++++++++++++++++++-- controllers/store_secrets.go | 12 +++++++++++- main.go | 18 +++++++++++++++--- models/user.go | 23 +++++++++++++++++++++++ 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 05c5408..25a0acb 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file +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 diff --git a/controllers/store_secrets.go b/controllers/store_secrets.go index dc608eb..04e7803 100644 --- a/controllers/store_secrets.go +++ b/controllers/store_secrets.go @@ -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{} diff --git a/main.go b/main.go index 63741d5..a782ebb 100644 --- a/main.go +++ b/main.go @@ -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) diff --git a/models/user.go b/models/user.go index c3693f0..499dee5 100644 --- a/models/user.go +++ b/models/user.go @@ -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 } /*