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,3 +1,7 @@
# 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.
@@ -5,3 +9,27 @@ Provide REST API for CRUD to store and retrieve user/password data for logging i
Requires JWT token to store/retrieve passwords.
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
}
/*