This commit is contained in:
ston1th 2023-03-21 01:00:13 +01:00
commit 05a68df896
15 changed files with 158 additions and 135 deletions

View file

@ -37,7 +37,7 @@ govet:
$(CC) vet ./... $(CC) vet ./...
misspell: misspell:
$(GOPATH)/bin/misspell cmd/* pkg/* Makefile README.md sri.sh $(GOPATH)/bin/misspell cmd/* pkg/* Makefile README.md
staticcheck: staticcheck:
$(GOPATH)/bin/staticcheck ./... $(GOPATH)/bin/staticcheck ./...

View file

@ -1 +1,41 @@
# KeyCtl - deliver secret keys with manual confirmation # KeyCtl - deliver secret keys with manual confirmation
## Server
```
./keyctl server
```
## Client
### Admin Access
Admin access is only allowed via the Unix Domain Socket connection.
```
export KEYCTL_ENDPOINT=unix:///var/keyctl/keyctl.sock
```
```
./keyctl ls
```
### Get Secret
The API to retrieve a secret can be called from any source IP.
```
export KEYCTL_ENDPOINT=http://127.0.0.1:7070
```
```
./keyctl get -id aabbccddeeff...
```
## Usage
```
./keyctl --help
./keyctl server --help
```

View file

@ -34,13 +34,13 @@ func usage() {
fmt.Printf(`keyctl %s: fmt.Printf(`keyctl %s:
supported subcommands: supported subcommands:
keyctl server - starts a keyctl server keyctl server - starts a keyctl server
keyctl new - generates a new key
keyctl get - retrieve a key from the server keyctl get - retrieve a key from the server
keyctl ls - list configured keys keyctl new - generates a new key (admin)
keyctl req - list current key requests keyctl ls - list configured keys (admin)
keyctl approve - approve a key request keyctl req - list current key requests (admin)
keyctl reject - reject a key request keyctl approve - approve a key request (admin)
keyctl del - delete a key keyctl reject - reject a key request (admin)
keyctl del - delete a key (admin)
keyctl help - show this usage keyctl help - show this usage
`, version) `, version)
os.Exit(1) os.Exit(1)
@ -55,27 +55,30 @@ func main() {
case "new", "get", "ls", "req", "approve", "reject", "del": case "new", "get", "ls", "req", "approve", "reject", "del":
cli.Run(arg) // does not return cli.Run(arg) // does not return
case "server": case "server":
fs := flag.NewFlagSet("", flag.ExitOnError) server() // does not return
klog.InitFlags(fs)
fs.StringVar(&listen, "listen", ":7070", "listen ip:port")
fs.StringVar(&socket, "socket", "keyctl.sock", "listen unix domain socket")
fs.StringVar(&path, "path", "/var/keyctl", "db storage dir")
fs.Parse(os.Args[2:])
default: default:
usage() usage() // does not return
} }
}
func server() {
fs := flag.NewFlagSet("", flag.ExitOnError)
klog.InitFlags(fs)
fs.StringVar(&listen, "listen", ":7070", "listen ip:port")
fs.StringVar(&socket, "socket", "keyctl.sock", "listen unix domain socket")
fs.StringVar(&path, "path", "/var/keyctl", "db storage dir")
fs.Parse(os.Args[2:])
log = klogr.New().WithName("main") log = klogr.New().WithName("main")
log.Info("starting keyctl", "version", version) log.Info("starting keyctl", "version", version)
srv, err := api.NewServer(klogr.New().WithName("srv"), listen, filepath.Join(path, socket))
if err != nil {
klog.Fatalf("init failed: %s", err)
}
dbstore, err := db.New(klogr.New().WithName("db"), path) dbstore, err := db.New(klogr.New().WithName("db"), path)
if err != nil { if err != nil {
klog.Fatalf("init failed: %s", err) klog.Fatalf("init failed: %s", err)
} }
srv.Start(dbstore) srv, err := api.NewServer(klogr.New().WithName("srv"), listen, filepath.Join(path, socket), dbstore)
if err != nil {
klog.Fatalf("init failed: %s", err)
}
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

View file

@ -34,21 +34,30 @@ type notFoundHandler struct {
} }
func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
types.NewContext(w, r, nf.s.Data, nf.s.log).Err(types.ErrInvalidAPIRoute) types.NewContext(w, r, nil, nf.s.log).Err(types.ErrInvalidAPIRoute)
} }
// NewHTTPServer returns a new HTTPServer // NewHTTPServer returns a new HTTPServer
func NewServer(log logr.Logger, listen, socket string) (*Server, error) { func NewServer(log logr.Logger, listen, socket string, db *db.DB) (*Server, error) {
m := mux.NewRouter()
s := &Server{ s := &Server{
mux: mux.NewRouter(), mux: m,
log: log, log: log,
Data: &types.ContextData{Approver: types.NewApprover()}, Data: &types.ContextData{
Approver: types.NewApprover(),
DB: db,
},
srv: &http.Server{
Handler: m,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
} }
l, err := net.Listen("tcp", listen) list, err := net.Listen("tcp", listen)
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.listen = l s.listen = list
sock, err := unixListener(socket) sock, err := unixListener(socket)
if err != nil { if err != nil {
@ -56,29 +65,11 @@ func NewServer(log logr.Logger, listen, socket string) (*Server, error) {
} }
s.socket = sock s.socket = sock
s.mux.NotFoundHandler = &notFoundHandler{s} m.NotFoundHandler = &notFoundHandler{s}
for _, v := range serverv1.Routes { for _, v := range serverv1.Routes {
s.mux.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...) m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
} }
return s, nil
}
func unixListener(socket string) (sock net.Listener, err error) {
sock, err = net.Listen("unix", socket)
if err != nil {
return
}
err = os.Chmod(socket, 0660)
return
}
func (s *Server) Start(db *db.DB) {
s.srv = &http.Server{
Handler: s.mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
s.Data.DB = db
go func() { go func() {
err := s.srv.Serve(s.listen) err := s.srv.Serve(s.listen)
if err != nil && err != http.ErrServerClosed { if err != nil && err != http.ErrServerClosed {
@ -91,6 +82,15 @@ func (s *Server) Start(db *db.DB) {
s.log.Error(err, "") s.log.Error(err, "")
} }
}() }()
return s, nil
}
func unixListener(socket string) (sock net.Listener, err error) {
sock, err = net.Listen("unix", socket)
if err != nil {
return
}
err = os.Chmod(socket, 0660)
return return
} }

View file

@ -4,7 +4,6 @@ package types
import ( import (
"sync" "sync"
"time"
"git.giftfish.de/ston1th/keyctl/pkg/key" "git.giftfish.de/ston1th/keyctl/pkg/key"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
@ -17,7 +16,8 @@ type ApprovalMap map[string]Approval
type Approver struct { type Approver struct {
// protects m // protects m
mu sync.RWMutex mu sync.RWMutex
m ApprovalMap
m ApprovalMap
} }
func NewApprover() *Approver { func NewApprover() *Approver {
@ -39,10 +39,9 @@ func (a *Approver) New(id, name, ip string) (aid string, err error) {
return return
} }
a.m[aid] = Approval{ a.m[aid] = Approval{
ID: id, ID: id,
Name: name, Name: name,
IP: ip, IP: ip,
Created: time.Now().Unix(),
} }
return return
} }
@ -77,11 +76,10 @@ func (a *Approver) Update(aid string, s Status) {
} }
type Approval struct { type Approval struct {
ID string ID string
Name string Name string
IP string IP string
Created int64 Status Status
Status Status
} }
type Status int type Status int

View file

@ -15,8 +15,8 @@ import (
) )
type ContextData struct { type ContextData struct {
DB *db.DB
Approver *Approver Approver *Approver
DB *db.DB
} }
func NewContext(w http.ResponseWriter, r *http.Request, data *ContextData, log logr.Logger) *Context { func NewContext(w http.ResponseWriter, r *http.Request, data *ContextData, log logr.Logger) *Context {
@ -84,9 +84,8 @@ func (c *Context) ClientIP() (host string) {
} }
// Var returns the given url variable // Var returns the given url variable
func (c *Context) Var(name string) (ret string) { func (c *Context) Var(name string) string {
ret, _ = mux.Vars(c.Request)[name] return mux.Vars(c.Request)[name]
return
} }
func (c *Context) log() { func (c *Context) log() {

View file

@ -6,7 +6,6 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"time"
"git.giftfish.de/ston1th/keyctl/pkg/api/types" "git.giftfish.de/ston1th/keyctl/pkg/api/types"
"git.giftfish.de/ston1th/keyctl/pkg/core" "git.giftfish.de/ston1th/keyctl/pkg/core"
@ -59,7 +58,6 @@ func ApproveURL(id, aid string) string {
type Key struct { type Key struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Created int64 `json:"created"`
Size int `json:"size"` Size int `json:"size"`
Encoding core.Encoding `json:"encoding"` Encoding core.Encoding `json:"encoding"`
Type core.Type `json:"type"` Type core.Type `json:"type"`
@ -92,15 +90,14 @@ func (Keys) Less(a, b Key) bool { return a.Name < b.Name }
func (k Keys) Sort() { slices.SortStableFunc(k, k.Less) } func (k Keys) Sort() { slices.SortStableFunc(k, k.Less) }
func (k Keys) Len() int { return len(k) } func (k Keys) Len() int { return len(k) }
func (k Keys) Fields() string { func (k Keys) Fields() string {
return "ID\t Name\t Created\t Size\t Encoding\t Type" return "ID\t Name\t Size\t Encoding\t Type"
} }
func (k Keys) Values() []string { func (k Keys) Values() []string {
vals := make([]string, len(k)) vals := make([]string, len(k))
for i, v := range k { for i, v := range k {
vals[i] = fmt.Sprintf("%s\t %s\t %s\t %d\t %s\t %s", vals[i] = fmt.Sprintf("%s\t %s\t %d\t %s\t %s",
v.ID, v.ID,
v.Name, v.Name,
timeFmt(v.Created),
v.Size, v.Size,
v.Encoding, v.Encoding,
v.Type, v.Type,
@ -118,12 +115,11 @@ func NewKeys(keys core.Keys) (k Keys) {
} }
type Approval struct { type Approval struct {
ID string `json:"id"` ID string `json:"id"`
AID string `json:"aid"` AID string `json:"aid"`
Name string `json:"name"` Name string `json:"name"`
IP string `json:"ip"` IP string `json:"ip"`
Created int64 `json:"created"` Status types.Status `json:"status"`
Status types.Status `json:"status"`
} }
type Approvals []Approval type Approvals []Approval
@ -132,17 +128,16 @@ func (Approvals) Less(a, b Approval) bool { return a.Name < b.Name }
func (a Approvals) Sort() { slices.SortStableFunc(a, a.Less) } func (a Approvals) Sort() { slices.SortStableFunc(a, a.Less) }
func (a Approvals) Len() int { return len(a) } func (a Approvals) Len() int { return len(a) }
func (a Approvals) Fields() string { func (a Approvals) Fields() string {
return "ID\t Approval ID\t Name\t Status\t Created\t IP" return "ID\t Approval ID\t Name\t Status\t IP"
} }
func (a Approvals) Values() []string { func (a Approvals) Values() []string {
vals := make([]string, len(a)) vals := make([]string, len(a))
for i, v := range a { for i, v := range a {
vals[i] = fmt.Sprintf("%s\t %s\t %s\t %s\t %s\t %s", vals[i] = fmt.Sprintf("%s\t %s\t %s\t %s\t %s",
v.ID, v.ID,
v.AID, v.AID,
v.Name, v.Name,
v.Status, v.Status,
timeFmt(v.Created),
v.IP, v.IP,
) )
} }
@ -152,18 +147,13 @@ func (a Approvals) Values() []string {
func NewApprovals(m types.ApprovalMap) (a Approvals) { func NewApprovals(m types.ApprovalMap) (a Approvals) {
for k, v := range m { for k, v := range m {
a = append(a, Approval{ a = append(a, Approval{
ID: v.ID, ID: v.ID,
AID: k, AID: k,
Name: v.Name, Name: v.Name,
IP: v.IP, IP: v.IP,
Created: v.Created, Status: v.Status,
Status: v.Status,
}) })
} }
a.Sort() a.Sort()
return return
} }
func timeFmt(t int64) string {
return time.Unix(t, 0).Format(time.RFC3339)
}

View file

@ -18,15 +18,6 @@ func local(h types.CtxHandler) types.CtxHandler {
h(ctx) h(ctx)
return return
} }
//addr, err := netip.ParseAddr(ip)
//if err != nil {
// ctx.Err(types.ErrForbidden)
// return
//}
//if addr.IsLoopback() {
// h(ctx)
// return
//}
ctx.Err(types.ErrForbidden) ctx.Err(types.ErrForbidden)
} }
} }

View file

@ -17,43 +17,43 @@ const (
var Routes = []types.Route{ var Routes = []types.Route{
{ {
healthz, Path: healthz,
healthzHandler, Handler: healthzHandler,
[]string{"GET"}, Methods: []string{"GET"},
}, },
{ {
schema.KeyPath, Path: schema.KeyPath,
local(keyListHandler), Handler: local(keyListHandler),
[]string{"GET"}, Methods: []string{"GET"},
}, },
{ {
schema.KeyPath, Path: schema.KeyPath,
local(newHandler), Handler: local(newHandler),
[]string{"POST"}, Methods: []string{"POST"},
}, },
{ {
keyID, Path: keyID,
keyHandler, Handler: keyHandler,
[]string{"GET"}, Methods: []string{"GET"},
}, },
{ {
keyReqID, Path: keyReqID,
keyHandler, Handler: keyHandler,
[]string{"GET"}, Methods: []string{"GET"},
}, },
{ {
keyID, Path: keyID,
local(keyHandler), Handler: local(keyHandler),
[]string{"DELETE"}, Methods: []string{"DELETE"},
}, },
{ {
schema.ReqPath, Path: schema.ReqPath,
local(reqListHandler), Handler: local(reqListHandler),
[]string{"GET"}, Methods: []string{"GET"},
}, },
{ {
reqID, Path: reqID,
local(reqHandler), Handler: local(reqHandler),
[]string{"PUT", "DELETE"}, Methods: []string{"PUT", "DELETE"},
}, },
} }

View file

@ -25,7 +25,6 @@ func exit(err error) {
} }
func Run(arg string) { func Run(arg string) {
var err error
ctx := context.Background() ctx := context.Background()
switch arg { switch arg {
case "new": case "new":
@ -58,6 +57,9 @@ func Run(arg string) {
} }
c := client() c := client()
err = c.DeleteKey(ctx, id) err = c.DeleteKey(ctx, id)
if err != nil {
exit(err)
}
case "approve": case "approve":
id, err := idFlags("approval id") id, err := idFlags("approval id")
if err != nil { if err != nil {
@ -65,6 +67,9 @@ func Run(arg string) {
} }
c := client() c := client()
err = c.AcceptApproval(ctx, id) err = c.AcceptApproval(ctx, id)
if err != nil {
exit(err)
}
case "reject": case "reject":
id, err := idFlags("approval id") id, err := idFlags("approval id")
if err != nil { if err != nil {
@ -72,6 +77,9 @@ func Run(arg string) {
} }
c := client() c := client()
err = c.RejectApproval(ctx, id) err = c.RejectApproval(ctx, id)
if err != nil {
exit(err)
}
case "get": case "get":
id, share, json, err := getFlags() id, share, json, err := getFlags()
if err != nil { if err != nil {
@ -114,9 +122,6 @@ func Run(arg string) {
} }
print(approvals) print(approvals)
} }
if err != nil {
exit(err)
}
os.Exit(0) os.Exit(0)
} }

View file

@ -12,6 +12,13 @@ var b32raw = base32.StdEncoding.WithPadding(base32.NoPadding)
type Encoding int type Encoding int
const (
Hex Encoding = iota
Base32
Base64
Base64URL
)
func (e Encoding) String() string { func (e Encoding) String() string {
switch e { switch e {
case Base32: case Base32:
@ -59,10 +66,3 @@ func EncodingFromString(e string) Encoding {
} }
return Hex return Hex
} }
const (
Hex Encoding = iota
Base32
Base64
Base64URL
)

View file

@ -17,7 +17,6 @@ var NameRe = regexp.MustCompile(nameReReverse)
type Key struct { type Key struct {
ID string ID string
Name string Name string
Created int64
Size int Size int
Encoding Encoding Encoding Encoding
Type Type Type Type

View file

@ -10,6 +10,11 @@ import (
type Type int type Type int
const (
Plain Type = iota
Shamir
)
func (t Type) String() string { func (t Type) String() string {
switch t { switch t {
case Shamir: case Shamir:
@ -48,8 +53,3 @@ func TypeFromString(t string) Type {
} }
return Plain return Plain
} }
const (
Plain Type = iota
Shamir
)

View file

@ -6,7 +6,6 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"io" "io"
"time"
"git.giftfish.de/ston1th/keyctl/pkg/core" "git.giftfish.de/ston1th/keyctl/pkg/core"
) )
@ -46,7 +45,6 @@ func Generate(name string, size int, enc core.Encoding, t core.Type) (kstore, k
Size: size, Size: size,
Encoding: enc, Encoding: enc,
Type: t, Type: t,
Created: time.Now().Unix(),
} }
k = kstore k = kstore
if t == core.Shamir { if t == core.Shamir {