Skip to content

Commit 8cbfcf5

Browse files
authored
api: TLS & Authentication (#4)
1 parent 10363b6 commit 8cbfcf5

File tree

9 files changed

+459
-219
lines changed

9 files changed

+459
-219
lines changed

.goreleaser.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ archives:
2525
- README*
2626
- LICENSE*
2727
- netbootd.service
28+
- netbootd.yml

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ mounts:
119119
# So that localDir: /tftpboot path: /subdir and client request: /subdir/file.x so that the host
120120
# path becomes /tfptboot/file.x
121121
localDir: /tftpboot
122-
# When true, the localDir path defined above gets a suffix to the Path prefix appended to it.
122+
# When true, the localDir path defined above gets a suffix to the Path prefix appended to it.
123123
appendSuffix: true
124124

125125
- path: /install.ipxe
@@ -225,8 +225,8 @@ Run e.g. `./netbootd --trace server -m ./examples/`
225225
226226
## Roadmap / TODOs
227227
228-
* API TLS & Authentication
229-
* Manifest persistence (currently API-configured manifests live in memory only)
230-
* Pluggable store backends (e.g. Redis, Etcd, files) for Manifests
231-
* Notifications (e.g. long-polling wait to return when a given host actually booted)
232-
* Per-manifest logs available over API
228+
* [x] API TLS & Authentication
229+
* [ ] Manifest persistence (currently API-configured manifests live in memory only)
230+
* [ ] Pluggable store backends (e.g. Redis, Etcd, files) for Manifests
231+
* [ ] Notifications (e.g. long-polling wait to return when a given host actually booted)
232+
* [ ] Per-manifest logs available over API

api/server.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ type Server struct {
2323
store *store.Store
2424
}
2525

26-
func NewServer(store *store.Store) (server *Server, err error) {
26+
// NewServer set up HTTP API server instance
27+
// If authorization is passed, requires privileged operation callers to present Authorization header with this content.
28+
func NewServer(store *store.Store, authorization string) (server *Server, err error) {
2729
r := mux.NewRouter()
2830

2931
server = &Server{
@@ -64,6 +66,11 @@ func NewServer(store *store.Store) (server *Server, err error) {
6466

6567
// GET /api/manifests
6668
r.HandleFunc("/api/manifests", func(w http.ResponseWriter, r *http.Request) {
69+
if authorization != r.Header.Get("Authorization") {
70+
http.Error(w, "Forbidden", http.StatusForbidden)
71+
return
72+
}
73+
6774
var b []byte
6875
if strings.Contains(r.Header.Get("Accept"), "application/json") {
6976
w.Header().Set("Content-Type", "applications/json")
@@ -78,6 +85,11 @@ func NewServer(store *store.Store) (server *Server, err error) {
7885

7986
// GET /api/manifests/{id}
8087
r.HandleFunc("/api/manifests/{id}", func(w http.ResponseWriter, r *http.Request) {
88+
if authorization != r.Header.Get("Authorization") {
89+
http.Error(w, "Forbidden", http.StatusForbidden)
90+
return
91+
}
92+
8193
vars := mux.Vars(r)
8294
m := store.Find(vars["id"])
8395
if m == nil {
@@ -98,6 +110,11 @@ func NewServer(store *store.Store) (server *Server, err error) {
98110

99111
// PUT /api/manifests/{id}
100112
r.HandleFunc("/api/manifests/{id}", func(w http.ResponseWriter, r *http.Request) {
113+
if authorization != r.Header.Get("Authorization") {
114+
http.Error(w, "Forbidden", http.StatusForbidden)
115+
return
116+
}
117+
101118
buf, _ := ioutil.ReadAll(r.Body)
102119
var m manifest.Manifest
103120
if r.Header.Get("Content-Type") == "application/json" {
@@ -119,6 +136,11 @@ func NewServer(store *store.Store) (server *Server, err error) {
119136

120137
// DELETE /api/manifests/{id}
121138
r.HandleFunc("/api/manifests/{id}", func(w http.ResponseWriter, r *http.Request) {
139+
if authorization != r.Header.Get("Authorization") {
140+
http.Error(w, "Forbidden", http.StatusForbidden)
141+
return
142+
}
143+
122144
vars := mux.Vars(r)
123145
store.ForgetManifest(vars["id"])
124146

@@ -129,6 +151,10 @@ func NewServer(store *store.Store) (server *Server, err error) {
129151
r.HandleFunc("/api/self/suspend-boot", func(w http.ResponseWriter, r *http.Request) {
130152
var ip net.IP
131153
if queryFirst(r, "spoof") != "" {
154+
if authorization != r.Header.Get("Authorization") {
155+
http.Error(w, "Forbidden", http.StatusForbidden)
156+
return
157+
}
132158
ip = net.ParseIP(queryFirst(r, "spoof"))
133159
} else {
134160
host, _, _ := net.SplitHostPort(r.RemoteAddr)
@@ -149,6 +175,10 @@ func NewServer(store *store.Store) (server *Server, err error) {
149175
r.HandleFunc("/api/self/unsuspend-boot", func(w http.ResponseWriter, r *http.Request) {
150176
var ip net.IP
151177
if queryFirst(r, "spoof") != "" {
178+
if authorization != r.Header.Get("Authorization") {
179+
http.Error(w, "Forbidden", http.StatusForbidden)
180+
return
181+
}
152182
ip = net.ParseIP(queryFirst(r, "spoof"))
153183
} else {
154184
host, _, _ := net.SplitHostPort(r.RemoteAddr)
@@ -169,6 +199,10 @@ func NewServer(store *store.Store) (server *Server, err error) {
169199
r.HandleFunc("/api/self/manifest", func(w http.ResponseWriter, r *http.Request) {
170200
var ip net.IP
171201
if queryFirst(r, "spoof") != "" {
202+
if authorization != r.Header.Get("Authorization") {
203+
http.Error(w, "Forbidden", http.StatusForbidden)
204+
return
205+
}
172206
ip = net.ParseIP(queryFirst(r, "spoof"))
173207
} else {
174208
host, _, _ := net.SplitHostPort(r.RemoteAddr)
@@ -199,6 +233,10 @@ func (server *Server) Serve(l net.Listener) error {
199233
return server.httpServer.Serve(l)
200234
}
201235

236+
func (server *Server) ServeTLS(l net.Listener, certFile string, keyFile string) error {
237+
return server.httpServer.ServeTLS(l, certFile, keyFile)
238+
}
239+
202240
func queryFirst(r *http.Request, k string) string {
203241
keys, ok := r.URL.Query()[k]
204242
if !ok || len(keys[0]) < 1 {

cmd/server.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/rs/zerolog"
1111
"github.com/rs/zerolog/log"
1212
"github.com/spf13/cobra"
13+
"github.com/spf13/viper"
1314
"net"
1415
"os"
1516
"os/signal"
@@ -20,16 +21,22 @@ var (
2021
ifname string
2122
httpPort int
2223
apiPort int
24+
apiTlsCert string
25+
apiTlsKey string
2326
manifestPath string
2427
)
2528

2629
func init() {
2730
serverCmd.Flags().StringVarP(&addr, "address", "a", "", "IP address to listen on (DHCP, TFTP, HTTP)")
2831
serverCmd.Flags().IntVarP(&httpPort, "http-port", "p", 8080, "HTTP port to listen on")
2932
serverCmd.Flags().IntVarP(&apiPort, "api-port", "r", 8081, "HTTP API port to listen on")
33+
serverCmd.Flags().StringVar(&apiTlsCert, "api-tls-cert", "", "Path to TLS certificate API")
34+
serverCmd.Flags().StringVar(&apiTlsKey, "api-tls-key", "", "Path to TLS certificate for API")
3035
serverCmd.Flags().StringVarP(&ifname, "interface", "i", "", "interface to listen on, e.g. eth0 (DHCP)")
3136
serverCmd.Flags().StringVarP(&manifestPath, "manifests", "m", "", "load manifests from directory")
3237

38+
viper.BindPFlag("api.TLSCertificatePath", serverCmd.Flags().Lookup("api-tls-cert"))
39+
viper.BindPFlag("api.TLSPrivateKeyPath", serverCmd.Flags().Lookup("api-tls-key"))
3340
rootCmd.AddCommand(serverCmd)
3441
}
3542

@@ -93,7 +100,7 @@ var serverCmd = &cobra.Command{
93100
log.Info().Interface("addr", connHttp.Addr()).Msg("HTTP listening")
94101

95102
// HTTP API service
96-
apiServer, err := api.NewServer(store)
103+
apiServer, err := api.NewServer(store, viper.GetString("api.authorization"))
97104
if err != nil {
98105
log.Fatal().Err(err)
99106
}
@@ -104,8 +111,23 @@ var serverCmd = &cobra.Command{
104111
if err != nil {
105112
log.Fatal().Err(err)
106113
}
107-
go apiServer.Serve(connApi)
108-
log.Info().Interface("api", connApi.Addr()).Msg("HTTP API listening")
114+
if viper.GetString("api.TLSCertificatePath") != "" && viper.GetString("api.TLSPrivateKeyPath") != "" {
115+
log.Info().Interface("api", connApi.Addr()).Msg("HTTP API listening with TLS...")
116+
go func() {
117+
err := apiServer.ServeTLS(connApi, viper.GetString("api.TLSCertificatePath"), viper.GetString("api.TLSPrivateKeyPath"))
118+
log.Error().Err(err).Msg("Error initializing TLS HTTP API listener!")
119+
}()
120+
} else {
121+
go apiServer.Serve(connApi)
122+
log.Info().Interface("api", connApi.Addr()).Msg("HTTP API listening...")
123+
go func() {
124+
go apiServer.Serve(connApi)
125+
log.Error().Err(err).Msg("Error initializing HTTP API listener!")
126+
}()
127+
}
128+
if !viper.IsSet("api.authorization") {
129+
log.Warn().Interface("api", connApi.Addr()).Msg("API is running without authentication, set Authorization in config!")
130+
}
109131

110132
// notify systemd
111133
sent, err := systemd.SdNotify(true, "READY=1\n")

config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
package config
22

33
type Config struct {
4+
Api struct {
5+
Authorization string
6+
TLSPrivateKeyPath string
7+
TLSCertificatePath string
8+
}
49
}

go.mod

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,18 @@ require (
1111
github.com/google/uuid v1.2.0 // indirect
1212
github.com/gorilla/mux v1.8.0
1313
github.com/huandu/xstrings v1.3.2 // indirect
14-
github.com/imdario/mergo v0.3.11 // indirect
15-
github.com/insomniacslk/dhcp v0.0.0-20210120172423-cc9239ac6294
16-
github.com/magiconair/properties v1.8.4 // indirect
17-
github.com/mitchellh/copystructure v1.1.1 // indirect
18-
github.com/mitchellh/mapstructure v1.4.1 // indirect
19-
github.com/pelletier/go-toml v1.8.1 // indirect
14+
github.com/imdario/mergo v0.3.12 // indirect
15+
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e
16+
github.com/mitchellh/copystructure v1.2.0 // indirect
2017
github.com/pin/tftp v2.1.0+incompatible
21-
github.com/rs/zerolog v1.20.0
22-
github.com/spf13/afero v1.5.1 // indirect
23-
github.com/spf13/cast v1.3.1 // indirect
18+
github.com/rs/zerolog v1.23.0
2419
github.com/spf13/cobra v1.1.3
25-
github.com/spf13/jwalterweatherman v1.1.0 // indirect
26-
github.com/spf13/viper v1.7.1
20+
github.com/spf13/viper v1.8.1
2721
github.com/u-root/u-root v7.0.0+incompatible
28-
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
29-
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d
30-
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43
31-
golang.org/x/text v0.3.5 // indirect
32-
gopkg.in/ini.v1 v1.62.0 // indirect
22+
github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 // indirect
23+
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
24+
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
25+
golang.org/x/sys v0.0.0-20210629170331-7dc0b73dc9fb
3326
gopkg.in/yaml.v2 v2.4.0
3427
)
3528

0 commit comments

Comments
 (0)