store download & add key
This commit is contained in:
@ -12,13 +12,15 @@ var (
|
||||
)
|
||||
|
||||
func casCleaner() {
|
||||
for {
|
||||
for range time.Tick(*cacheCleanDelay) {
|
||||
if !wPublicState.Get().Store.Open {
|
||||
continue
|
||||
}
|
||||
|
||||
err := cleanCAS()
|
||||
if err != nil {
|
||||
log.Print("warn: couldn't clean cache: ", err)
|
||||
}
|
||||
|
||||
time.Sleep(*cacheCleanDelay)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,4 +6,7 @@ import (
|
||||
"m.cluseau.fr/go/httperr"
|
||||
)
|
||||
|
||||
var ErrNotFound = httperr.NewStd(404, http.StatusNotFound, "not found")
|
||||
var (
|
||||
ErrNotFound = httperr.NewStd(404, http.StatusNotFound, "not found")
|
||||
ErrInvalidToken = httperr.NewStd(403, http.StatusForbidden, "invalid token")
|
||||
)
|
||||
|
@ -53,7 +53,7 @@ func main() {
|
||||
if autoUnlock != "" {
|
||||
log.Printf("auto-unlocking the store")
|
||||
err := unlockSecretStore([]byte(autoUnlock))
|
||||
if err != nil {
|
||||
if err.Any() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -20,7 +18,8 @@ import (
|
||||
|
||||
var secStore *secretstore.Store
|
||||
|
||||
func secStorePath(name string) string { return filepath.Join(*dataDir, "secrets", name) }
|
||||
func secStoreRoot() string { return filepath.Join(*dataDir, "secrets") }
|
||||
func secStorePath(name string) string { return filepath.Join(secStoreRoot(), name) }
|
||||
func secKeysStorePath() string { return secStorePath(".keys") }
|
||||
|
||||
func openSecretStore() {
|
||||
@ -64,7 +63,7 @@ var (
|
||||
ErrInvalidPassphrase = httperr.NewStd(http.StatusBadRequest, 2, "invalid passphrase")
|
||||
)
|
||||
|
||||
func unlockSecretStore(passphrase []byte) *httperr.Error {
|
||||
func unlockSecretStore(passphrase []byte) (err httperr.Error) {
|
||||
unlockMutex.Lock()
|
||||
defer unlockMutex.Unlock()
|
||||
|
||||
@ -75,7 +74,7 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
|
||||
if secStore.IsNew() {
|
||||
err := secStore.Init(passphrase)
|
||||
if err != nil {
|
||||
return httperr.New(http.StatusInternalServerError, err)
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
err = secStore.SaveTo(secKeysStorePath())
|
||||
@ -83,7 +82,7 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
|
||||
log.Print("secret store save error: ", err)
|
||||
secStore.Close()
|
||||
|
||||
return httperr.New(http.StatusInternalServerError, err)
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
} else {
|
||||
@ -98,25 +97,21 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
|
||||
log.Print("failed to read admin token: ", err)
|
||||
secStore.Close()
|
||||
|
||||
return httperr.New(http.StatusInternalServerError, err)
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
randBytes := make([]byte, 32)
|
||||
_, err := rand.Read(randBytes)
|
||||
token, err = newToken(32)
|
||||
if err != nil {
|
||||
log.Print("rand read error: ", err)
|
||||
secStore.Close()
|
||||
|
||||
return httperr.New(http.StatusInternalServerError, err)
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randBytes)
|
||||
err = writeSecret("admin-token", token)
|
||||
if err != nil {
|
||||
log.Print("write error: ", err)
|
||||
secStore.Close()
|
||||
|
||||
return httperr.New(http.StatusInternalServerError, err)
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
log.Print("wrote new admin token")
|
||||
@ -124,6 +119,18 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
|
||||
|
||||
*adminToken = token
|
||||
|
||||
{
|
||||
token, err := newToken(16)
|
||||
if err != nil {
|
||||
secStore.Close()
|
||||
return httperr.Internal(err)
|
||||
}
|
||||
|
||||
wState.Change(func(v *State) {
|
||||
v.Store.DownloadToken = token
|
||||
})
|
||||
}
|
||||
|
||||
wPublicState.Change(func(v *PublicState) {
|
||||
v.Store.New = false
|
||||
v.Store.Open = true
|
||||
@ -132,7 +139,7 @@ func unlockSecretStore(passphrase []byte) *httperr.Error {
|
||||
go updateState()
|
||||
go migrateSecrets()
|
||||
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func readSecret(name string, value any) (err error) {
|
||||
@ -179,12 +186,12 @@ func writeSecret(name string, value any) (err error) {
|
||||
}
|
||||
|
||||
err = os.Rename(f.Name(), secStorePath(name+".data"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go updateState()
|
||||
return
|
||||
go updateState()
|
||||
return
|
||||
}
|
||||
|
||||
var secL sync.Mutex
|
||||
@ -255,7 +262,7 @@ func (s KVSecrets[T]) Keys(prefix string) (keys []string, err error) {
|
||||
keys = append(keys, k[len(prefix):])
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
sort.Strings(keys)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -20,6 +20,10 @@ var wPublicState = watchable.New[PublicState]()
|
||||
type State struct {
|
||||
HasConfig bool
|
||||
|
||||
Store struct {
|
||||
DownloadToken string
|
||||
}
|
||||
|
||||
Clusters []ClusterState
|
||||
Hosts []HostState
|
||||
Config *localconfig.Config
|
||||
|
24
cmd/dkl-local-server/token.go
Normal file
24
cmd/dkl-local-server/token.go
Normal file
@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"m.cluseau.fr/go/httperr"
|
||||
)
|
||||
|
||||
func newToken(sizeInBytes int) (token string, err error) {
|
||||
randBytes := make([]byte, sizeInBytes)
|
||||
|
||||
_, err = rand.Read(randBytes)
|
||||
if err != nil {
|
||||
log.Print("rand read error: ", err)
|
||||
err = httperr.New(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randBytes)
|
||||
return
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
@ -14,10 +19,80 @@ func wsUnlockStore(req *restful.Request, resp *restful.Response) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := unlockSecretStore([]byte(passphrase)); err != nil {
|
||||
if err := unlockSecretStore([]byte(passphrase)); err.Any() {
|
||||
err.WriteJSON(resp.ResponseWriter)
|
||||
return
|
||||
}
|
||||
|
||||
resp.WriteEntity(*adminToken)
|
||||
}
|
||||
|
||||
func wsStoreDownload(req *restful.Request, resp *restful.Response) {
|
||||
token := req.QueryParameter("token")
|
||||
if token != wState.Get().Store.DownloadToken {
|
||||
wsError(resp, ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
arch := tar.NewWriter(buf)
|
||||
|
||||
root := os.DirFS(secStoreRoot())
|
||||
|
||||
err := fs.WalkDir(root, ".", func(path string, d fs.DirEntry, readErr error) (err error) {
|
||||
if readErr != nil {
|
||||
err = readErr
|
||||
return
|
||||
}
|
||||
|
||||
if path == "." {
|
||||
return
|
||||
}
|
||||
|
||||
fi, err := d.Info()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hdr, err := tar.FileInfoHeader(fi, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hdr.Name = path
|
||||
hdr.Uid = 0
|
||||
hdr.Gid = 0
|
||||
|
||||
err = arch.WriteHeader(hdr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := root.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
io.Copy(arch, f)
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = arch.Close()
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
|
||||
buf.WriteTo(resp)
|
||||
}
|
||||
|
27
cmd/dkl-local-server/ws-store.go
Normal file
27
cmd/dkl-local-server/ws-store.go
Normal file
@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
restful "github.com/emicklei/go-restful"
|
||||
)
|
||||
|
||||
func wsStoreAddKey(req *restful.Request, resp *restful.Response) {
|
||||
var passphrase string
|
||||
|
||||
err := req.ReadEntity(&passphrase)
|
||||
if err != nil {
|
||||
wsBadRequest(resp, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(passphrase) == 0 {
|
||||
wsBadRequest(resp, "no passphrase given")
|
||||
return
|
||||
}
|
||||
|
||||
secStore.AddKey([]byte(passphrase))
|
||||
err = secStore.SaveTo(secKeysStorePath())
|
||||
if err != nil {
|
||||
wsError(resp, err)
|
||||
return
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@ -10,6 +11,7 @@ import (
|
||||
|
||||
cfsslconfig "github.com/cloudflare/cfssl/config"
|
||||
"github.com/emicklei/go-restful"
|
||||
"m.cluseau.fr/go/httperr"
|
||||
|
||||
"novit.tech/direktil/pkg/localconfig"
|
||||
|
||||
@ -28,6 +30,10 @@ func registerWS(rest *restful.Container) {
|
||||
Reads("").
|
||||
Writes("").
|
||||
Doc("Try to unlock the store")).
|
||||
Route(ws.GET("/store.tar").To(wsStoreDownload).
|
||||
Produces(mime.TAR).
|
||||
Param(ws.QueryParameter("token", "the download token")).
|
||||
Doc("Fetch the encrypted store")).
|
||||
Route(ws.GET("/downloads/{token}/{asset}").To(wsDownload).
|
||||
Param(ws.PathParameter("token", "the download token")).
|
||||
Param(ws.PathParameter("asset", "the requested asset")).
|
||||
@ -42,12 +48,21 @@ func registerWS(rest *restful.Container) {
|
||||
Filter(adminAuth).
|
||||
HeaderParameter("Authorization", "Admin bearer token")
|
||||
|
||||
// - store management
|
||||
ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey).
|
||||
Consumes("application/json").Reads("").
|
||||
Produces("application/json").
|
||||
Doc("Add an unlock key to the store"))
|
||||
|
||||
// - downloads
|
||||
ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload).
|
||||
Consumes("application/json").Reads(DownloadSpec{}).
|
||||
Produces("application/json").
|
||||
Doc("Create a download token for the given download"))
|
||||
|
||||
// - configs API
|
||||
ws.Route(ws.POST("/configs").To(wsUploadConfig).
|
||||
Consumes(mime.YAML).
|
||||
Doc("Upload a new current configuration, archiving the previous one"))
|
||||
|
||||
// - clusters API
|
||||
@ -180,11 +195,20 @@ func wsNotFound(req *restful.Request, resp *restful.Response) {
|
||||
http.NotFound(resp.ResponseWriter, req.Request)
|
||||
}
|
||||
|
||||
func wsBadRequest(resp *restful.Response, err string) {
|
||||
httperr.New(http.StatusBadRequest, errors.New(err)).WriteJSON(resp.ResponseWriter)
|
||||
}
|
||||
|
||||
func wsError(resp *restful.Response, err error) {
|
||||
log.Output(2, fmt.Sprint("request failed: ", err))
|
||||
resp.WriteErrorString(
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError))
|
||||
|
||||
switch err := err.(type) {
|
||||
case httperr.Error:
|
||||
err.WriteJSON(resp.ResponseWriter)
|
||||
|
||||
default:
|
||||
httperr.Internal(err).WriteJSON(resp.ResponseWriter)
|
||||
}
|
||||
}
|
||||
|
||||
func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value interface{}) {
|
||||
|
Reference in New Issue
Block a user