This commit is contained in:
34
.drone.yml
34
.drone.yml
@@ -9,8 +9,42 @@ steps:
|
||||
- name: build
|
||||
image: golang
|
||||
commands:
|
||||
- go get -u github.com/gomarkdown/mdtohtml
|
||||
- mdtohtml README.md index.htm
|
||||
- sh ./.drone.sh
|
||||
|
||||
# Copy binary to test server
|
||||
- name: coadcorp-deploy
|
||||
image: appleboy/drone-scp
|
||||
settings:
|
||||
host:
|
||||
- 10.63.39.130
|
||||
username: l075239
|
||||
password:
|
||||
from_secret: ssh_password
|
||||
port: 22
|
||||
command_timeout: 2m
|
||||
target: /home/l075239/smt/
|
||||
source:
|
||||
- smt
|
||||
- cbs_checksum.txt
|
||||
- test.env
|
||||
|
||||
# Start service
|
||||
- name: coadcorp-restart
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host:
|
||||
- 10.63.39.130
|
||||
username: l075239
|
||||
password:
|
||||
from_secret: ssh_password
|
||||
port: 22
|
||||
command_timeout: 2m
|
||||
script:
|
||||
- sudo basc -c 'mv /home/l075239/smt/test.env /home/l075239/smt/.env'
|
||||
- sudo bash -c '/etc/init.d/cbs restart'
|
||||
|
||||
- name: dell-deploy
|
||||
# # https://github.com/cschlosser/drone-ftps/blob/master/README.md
|
||||
image: cschlosser/drone-ftps
|
||||
|
15
README.md
15
README.md
@@ -1,12 +1,17 @@
|
||||
# Secrets Management Tool (SMT)
|
||||
|
||||
Build Date: {BUILDTIME}
|
||||
Build Hash: {SHA1VER}
|
||||
|
||||
## Overview
|
||||
|
||||
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 secrets. Only password is encrypted, via AES256 GCM. Values stored encrypted within a sqlite database.
|
||||
|
||||
Requires JWT token to store/retrieve passwords.
|
||||
A successful authentication returns a JWT token which must be provided for all other operations.
|
||||
|
||||
This isn't super secure, probably not even as secure as Hashicorp Vault running in dev mode.
|
||||
Multiple user roles are supported, with each user only able to access secrets matching their user role. One exception is the built in administrator role that is able to access all secrets.
|
||||
|
||||
Written by Nathan Coad (nathan.coad@dell.com)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -18,7 +23,7 @@ This isn't super secure, probably not even as secure as Hashicorp Vault running
|
||||
## 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
|
||||
| LOG_FILE | Specify the name/path of file to write log messages to | /var/log/smt.log | ./smt.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 |
|
||||
@@ -30,6 +35,8 @@ This isn't super secure, probably not even as secure as Hashicorp Vault running
|
||||
|
||||
If the TLS certificate and key files cannot be located in the specified location, a self signed certificate will be generated with a 1 year validity period.
|
||||
|
||||
Example for generating API_SECRET and SECRETS_KEY is the following command on linux: `head /dev/urandom | tr -dc A-Za-z0-9 | head -c32`
|
||||
|
||||
## Systemd script
|
||||
|
||||
Create/update the systemd service definition at /etc/systemd/system/smt.service and then run systemctl daemon-reload
|
||||
|
253
index.htm
Normal file
253
index.htm
Normal file
@@ -0,0 +1,253 @@
|
||||
<h1>Secrets Management Tool (SMT)</h1>
|
||||
|
||||
<p>Build Date: {BUILDTIME}
|
||||
Build Hash: {SHA1VER}</p>
|
||||
|
||||
<h2>Overview</h2>
|
||||
|
||||
<p>Provide REST API for CRUD to store and retrieve secrets. Only password is encrypted, via AES256 GCM. Values stored encrypted within a sqlite database.</p>
|
||||
|
||||
<p>A successful authentication returns a JWT token which must be provided for all other operations.</p>
|
||||
|
||||
<p>Multiple user roles are supported, with each user only able to access secrets matching their user role. One exception is the built in administrator role that is able to access all secrets.</p>
|
||||
|
||||
<p>Written by Nathan Coad (nathan.coad@dell.com)</p>
|
||||
|
||||
<h2>Installation</h2>
|
||||
|
||||
<ol>
|
||||
<li>Copy binary to chosen location, eg /srv/smt/smt</li>
|
||||
<li>Create .env file in same directory as binary, populate as per Configuration section below</li>
|
||||
<li>Create systemd service definition</li>
|
||||
<li>Start service</li>
|
||||
</ol>
|
||||
|
||||
<h2>Configuration</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable Name</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>LOG_FILE</td>
|
||||
<td>Specify the name/path of file to write log messages to</td>
|
||||
<td>/var/log/smt.log</td>
|
||||
<td>./smt.log</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>BIND_IP</td>
|
||||
<td>Specify the local IP address to bind to.</td>
|
||||
<td>127.0.0.1</td>
|
||||
<td>Primary IPv4 address</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>BIND_PORT</td>
|
||||
<td>Specify the TCP/IP port to bind to.</td>
|
||||
<td>443</td>
|
||||
<td>8443</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>TLS_KEY_FILE</td>
|
||||
<td>Specify the filename of the TLS certificate private key (must be unencrypted) in PEM format</td>
|
||||
<td>key.pem</td>
|
||||
<td>privkey.pem</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>TLS_CERT_FILE</td>
|
||||
<td>Specify the filename of the TLS certificate file in PEM format</td>
|
||||
<td>cert.pem</td>
|
||||
<td>cert.pem</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>TOKEN_HOUR_LIFESPAN</td>
|
||||
<td>Number of hours that the JWT token returned at login is valid</td>
|
||||
<td>12</td>
|
||||
<td>No default specified, must define this value</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>API_SECRET</td>
|
||||
<td>Secret to use when generating JWT token</td>
|
||||
<td>3c55990bd479322e2053db3a8</td>
|
||||
<td>No default specified, must define this value</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>INITIAL_PASSWORD</td>
|
||||
<td>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</td>
|
||||
<td>$2a$10$s39a82wrRAdOJVZEkkrSReVnXprz5mxU30ZBO.dHPYTncQCsUD9ce</td>
|
||||
<td>password</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>SECRETS_KEY</td>
|
||||
<td>Key to use for AES256 GCM encryption. Must be exactly 32 bytes</td>
|
||||
<td>AES256Key-32Characters1234567890</td>
|
||||
<td>No default specified, must define this value or use /api/unlock at runtime</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>If the TLS certificate and key files cannot be located in the specified location, a self signed certificate will be generated with a 1 year validity period.</p>
|
||||
|
||||
<p>Example for generating API_SECRET and SECRETS_KEY is the following command on linux: <code>head /dev/urandom | tr -dc A-Za-z0-9 | head -c32</code></p>
|
||||
|
||||
<h2>Systemd script</h2>
|
||||
|
||||
<p>Create/update the systemd service definition at /etc/systemd/system/smt.service and then run systemctl daemon-reload</p>
|
||||
|
||||
<pre><code>[Unit]
|
||||
Description=Secrets Management Tool
|
||||
After=network.target
|
||||
#StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
User=root
|
||||
ExecStart=/srv/smt/smt
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
</code></pre>
|
||||
|
||||
<h2>API</h2>
|
||||
|
||||
<h3>Unlock</h3>
|
||||
|
||||
<p>POST <code>/api/unlock</code></p>
|
||||
|
||||
<p>Data</p>
|
||||
|
||||
<pre><code>{
|
||||
"secretKey": "Example32ByteSecretKey0123456789"
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p>If the SECRETS_KEY environment variable is not defined, this API call to unlock stored secrets must be performed after initial startup of SMT. Storing/retrieval of secrets will not succeed until this API call has been made.</p>
|
||||
|
||||
<h3>User Operations</h3>
|
||||
|
||||
<h4>Register</h4>
|
||||
|
||||
<p>POST <code>/api/admin/register</code></p>
|
||||
|
||||
<p>Data</p>
|
||||
|
||||
<pre><code>{
|
||||
"username": "",
|
||||
"password": "",
|
||||
"RoleId": 2
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p>This operation can only be performed by a user with a role that is admin enabled. There are 3 built in roles, which can be viewed via the <code>/api/admin/roles</code> endpoint.</p>
|
||||
|
||||
<h4>Login</h4>
|
||||
|
||||
<p>POST <code>/api/login</code></p>
|
||||
|
||||
<p>Data</p>
|
||||
|
||||
<pre><code>{
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p>This API call will return a JWT token that must be present for any other API calls to succeed. The validity duration of this token is based on the configured TOKEN_HOUR_LIFESPAN value. JWT token is returned as value of <code>access_token</code>.</p>
|
||||
|
||||
<h4>List Roles</h4>
|
||||
|
||||
<p>GET <code>/api/admin/roles</code></p>
|
||||
|
||||
<p>This operation can only be performed by a user with a role that is admin enabled. Lists currently defined roles.</p>
|
||||
|
||||
<h4>List Users</h4>
|
||||
|
||||
<p>GET <code>/api/admin/users</code></p>
|
||||
|
||||
<p>This operation can only be performed by a user with a role that is admin enabled. Lists currently defined users.</p>
|
||||
|
||||
<h3>Secrets Operations</h3>
|
||||
|
||||
<h4>Store</h4>
|
||||
|
||||
<p>POST <code>/api/secret/store</code></p>
|
||||
|
||||
<p>Data</p>
|
||||
|
||||
<pre><code>{
|
||||
"deviceName": "",
|
||||
"deviceCategory": "",
|
||||
"userName": "",
|
||||
"secretValue": ""
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p>Must be logged in to execute this command. Role of current user cannot be a ReadOnly role. Secret will be stored with the RoleId of the currently logged in user. Either deviceName or deviceCategory can be blank but not both.</p>
|
||||
|
||||
<p>If a secret exists for this RoleId and matching deviceName and deviceCategory then an error will be generated.</p>
|
||||
|
||||
<h4>Retrieve</h4>
|
||||
|
||||
<p>POST <code>/api/secret/retrieve</code></p>
|
||||
|
||||
<p>Data</p>
|
||||
|
||||
<pre><code>{
|
||||
"deviceName": "",
|
||||
"deviceCategory": "",
|
||||
"userName": ""
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p>Must be logged in to execute this command. Only secrets registered with the current user’s RoleId can be retrieved.</p>
|
||||
|
||||
<p>Either deviceName or deviceCategory can be specified (or both). Wildcards are supported for both deviceName and deviceCategory fields. userName can also be specified in conjunction with deviceName or deviceCategory.
|
||||
1. The percent sign % wildcard matches any sequence of zero or more characters.
|
||||
2. The underscore _ wildcard matches any single character.</p>
|
||||
|
||||
<p>GET <code>/api/secret/retrieve/name/<searchname></code></p>
|
||||
|
||||
<p>Search for a secret specified by deviceName using a GET request.
|
||||
Must be logged in to execute this command. Only secrets registered with the current user’s RoleId can be retrieved.</p>
|
||||
|
||||
<p>GET <code>/api/secret/retrieve/category/<searchname></code></p>
|
||||
|
||||
<p>Search for a secret specified by deviceCategory using a GET request.
|
||||
Must be logged in to execute this command. Only secrets registered with the current user’s RoleId can be retrieved.</p>
|
||||
|
||||
<h4>Update</h4>
|
||||
|
||||
<p>POST <code>/api/secret/update</code></p>
|
||||
|
||||
<p>Data</p>
|
||||
|
||||
<pre><code>{
|
||||
"deviceName": "",
|
||||
"deviceCategory": "",
|
||||
"userName": "",
|
||||
"secretValue": ""
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<p>Users with ReadOnly role will receive Forbidden error when calling this API endpoint. The values specified in deviceName and deviceCategory must match exactly one existing secret record for the RoleId of the currently logged in user. Wildcards are supported for deviceName and deviceCategory.</p>
|
||||
|
||||
<h4>List</h4>
|
||||
|
||||
<p>GET <code>/api/secret/list</code></p>
|
||||
|
||||
<p>Will generate a list of device names and categories but not secret data.</p>
|
66
main.go
66
main.go
@@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -23,10 +25,61 @@ import (
|
||||
var sha1ver string // sha1 revision used to build the program
|
||||
var buildTime string // when the executable was built
|
||||
|
||||
var keyString string
|
||||
type Replacements map[string]string
|
||||
|
||||
var replacements = make(Replacements)
|
||||
|
||||
//go:embed index.htm
|
||||
var staticContent embed.FS
|
||||
|
||||
// staticFileServer serves files from the provided fs.FS
|
||||
func staticFileServer(content embed.FS) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Get the requested file name
|
||||
fileName := c.Request.URL.Path
|
||||
|
||||
// Root request should load index.htm
|
||||
if len(fileName) == 1 {
|
||||
log.Printf("staticFileServer replacing root request with index.htm")
|
||||
fileName = "index.htm"
|
||||
}
|
||||
|
||||
//log.Printf("staticFileServer attempting to load filename '%s'\n", fileName)
|
||||
|
||||
// Try to open the file from the embedded FS
|
||||
file, err := content.Open(fileName)
|
||||
if err != nil {
|
||||
c.String(http.StatusNotFound, "File not found")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Serve the file contents
|
||||
fileStat, _ := file.Stat()
|
||||
data := make([]byte, fileStat.Size())
|
||||
_, err = file.Read(data)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Error reading file")
|
||||
return
|
||||
}
|
||||
|
||||
// parse the file and perform text replacements as necessary
|
||||
for key, element := range replacements {
|
||||
log.Printf("Searching for '%s' to replace\n", key)
|
||||
data = bytes.Replace(data, []byte(key), []byte(element), -1)
|
||||
}
|
||||
|
||||
// Set the proper Content-Type header based on file extension
|
||||
c.Header("Content-Type", http.DetectContentType(data))
|
||||
c.Data(http.StatusOK, "", data)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
replacements["{SHA1VER}"] = sha1ver
|
||||
replacements["{BUILDTIME}"] = buildTime
|
||||
|
||||
// Load data from environment file
|
||||
envFilename := utils.GetFilePath(".env")
|
||||
err := godotenv.Load(envFilename)
|
||||
@@ -53,7 +106,7 @@ func main() {
|
||||
models.ConnectDatabase()
|
||||
|
||||
// Set secrets key from .env file
|
||||
keyString = os.Getenv("SECRETS_KEY")
|
||||
keyString := os.Getenv("SECRETS_KEY")
|
||||
|
||||
if keyString != "" {
|
||||
// Key was defined in environment variable, let the models package know our secrets key
|
||||
@@ -83,10 +136,12 @@ func main() {
|
||||
// Recovery middleware recovers from any panics and writes a 500 if there was one.
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
/*
|
||||
// TODO - think of a better default landing page
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, fmt.Sprintf("SMT Built on %s from sha1 %s\n", buildTime, sha1ver))
|
||||
})
|
||||
*/
|
||||
|
||||
// Set some options for TLS
|
||||
tlsConfig := &tls.Config{
|
||||
@@ -144,6 +199,13 @@ func main() {
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
// Set the default readme page
|
||||
//router.Use(EmbedReact("/", "static_files", staticDir))
|
||||
//router.Use(static.Serve("/", static.LocalFile("./static_files", true)))
|
||||
|
||||
// Serve the embedded HTML file if no other routes match
|
||||
router.NoRoute(staticFileServer(staticContent))
|
||||
|
||||
// Register our routes
|
||||
public := router.Group("/api")
|
||||
public.POST("/login", controllers.Login)
|
||||
|
Reference in New Issue
Block a user