Skip to content

Commit fb9324d

Browse files
authored
Merge pull request #799 from jmpsec/gitops-osquery-configuration
Configuration endpoints to receive osquery configuration from gitOps
2 parents 0e526fa + 3b6c887 commit fb9324d

12 files changed

Lines changed: 255 additions & 43 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ repos:
2727
- id: go-unit-tests
2828

2929
- repo: https://github.com/golangci/golangci-lint
30-
rev: v1.62.2
30+
rev: v2.11.4
3131
hooks:
3232
- id: golangci-lint
3333
args: [--config=.golangci.yml]

cmd/admin/handlers/post.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,11 @@ func (h *HandlersAdmin) ConfPOSTHandler(w http.ResponseWriter, r *http.Request)
442442
adminErrorResponse(w, "invalid CSRF token", http.StatusInternalServerError, nil)
443443
return
444444
}
445+
// Check if configuration is read-only
446+
if h.OsqueryValues.ReadOnly {
447+
adminErrorResponse(w, "configuration is read-only", http.StatusForbidden, nil)
448+
return
449+
}
445450
if c.ConfigurationB64 != "" {
446451
// Base64 decode received configuration
447452
// TODO verify configuration

cmd/admin/templates/conf.html

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,12 @@
9898
</div>
9999
<div class="card-body">
100100

101-
<textarea id="options_conf" name="options_conf">{{ .Environment.Options }}</textarea>
101+
{{ if $leftmeta.OsqueryValues.ReadOnly }}
102+
<div class="alert alert-warning py-2 mb-2" role="alert">
103+
<i class="fas fa-lock mr-1"></i> Editing is disabled by server configuration.
104+
</div>
105+
{{ end }}
106+
<textarea id="options_conf" name="options_conf" {{ if $leftmeta.OsqueryValues.ReadOnly }}readonly{{ end }}>{{ .Environment.Options }}</textarea>
102107
<div class="row">
103108
<div class="col-md-12">
104109
<button id="options_json_status_color" class="text-left btn btn-sm btn-square btn-block btn-success disabled">
@@ -136,7 +141,12 @@
136141
</div>
137142
<div class="card-body">
138143

139-
<textarea id="schedule_conf" name="schedule_conf">{{ .Environment.Schedule }}</textarea>
144+
{{ if $leftmeta.OsqueryValues.ReadOnly }}
145+
<div class="alert alert-warning py-2 mb-2" role="alert">
146+
<i class="fas fa-lock mr-1"></i> Editing is disabled by server configuration.
147+
</div>
148+
{{ end }}
149+
<textarea id="schedule_conf" name="schedule_conf" {{ if $leftmeta.OsqueryValues.ReadOnly }}readonly{{ end }}>{{ .Environment.Schedule }}</textarea>
140150
<div class="row">
141151
<div class="col-md-12">
142152
<button id="schedule_json_status_color" class="text-left btn btn-sm btn-square btn-block btn-success disabled">
@@ -170,7 +180,12 @@
170180
</div>
171181
<div class="card-body">
172182

173-
<textarea id="packs_conf" name="packs_conf">{{ .Environment.Packs }}</textarea>
183+
{{ if $leftmeta.OsqueryValues.ReadOnly }}
184+
<div class="alert alert-warning py-2 mb-2" role="alert">
185+
<i class="fas fa-lock mr-1"></i> Editing is disabled by server configuration.
186+
</div>
187+
{{ end }}
188+
<textarea id="packs_conf" name="packs_conf" {{ if $leftmeta.OsqueryValues.ReadOnly }}readonly{{ end }}>{{ .Environment.Packs }}</textarea>
174189
<div class="row">
175190
<div class="col-md-12">
176191
<button id="packs_json_status_color" class="text-left btn btn-sm btn-square btn-block btn-success disabled">
@@ -204,7 +219,12 @@
204219
</div>
205220
<div class="card-body">
206221

207-
<textarea id="atc_conf" name="atc_conf">{{ .Environment.ATC }}</textarea>
222+
{{ if $leftmeta.OsqueryValues.ReadOnly }}
223+
<div class="alert alert-warning py-2 mb-2" role="alert">
224+
<i class="fas fa-lock mr-1"></i> Editing is disabled by server configuration.
225+
</div>
226+
{{ end }}
227+
<textarea id="atc_conf" name="atc_conf" {{ if $leftmeta.OsqueryValues.ReadOnly }}readonly{{ end }}>{{ .Environment.ATC }}</textarea>
208228
<div class="row">
209229
<div class="col-md-12">
210230
<button id="atc_json_status_color" class="text-left btn btn-sm btn-square btn-block btn-success disabled">
@@ -238,7 +258,12 @@
238258
</div>
239259
<div class="card-body">
240260

241-
<textarea id="decorators_conf" name="decorators_conf">{{ .Environment.Decorators }}</textarea>
261+
{{ if $leftmeta.OsqueryValues.ReadOnly }}
262+
<div class="alert alert-warning py-2 mb-2" role="alert">
263+
<i class="fas fa-lock mr-1"></i> Editing is disabled by server configuration.
264+
</div>
265+
{{ end }}
266+
<textarea id="decorators_conf" name="decorators_conf" {{ if $leftmeta.OsqueryValues.ReadOnly }}readonly{{ end }}>{{ .Environment.Decorators }}</textarea>
242267
<div class="row">
243268
<div class="col-md-12">
244269
<button id="decorators_json_status_color" class="text-left btn btn-sm btn-square btn-block btn-success disabled">
@@ -272,7 +297,12 @@
272297
</div>
273298
<div class="card-body">
274299

275-
<textarea id="final_conf" name="final_conf">{{ .Environment.Configuration }}</textarea>
300+
{{ if $leftmeta.OsqueryValues.ReadOnly }}
301+
<div class="alert alert-warning py-2 mb-2" role="alert">
302+
<i class="fas fa-lock mr-1"></i> Editing is disabled by server configuration.
303+
</div>
304+
{{ end }}
305+
<textarea id="final_conf" name="final_conf" {{ if $leftmeta.OsqueryValues.ReadOnly }}readonly{{ end }}>{{ .Environment.Configuration }}</textarea>
276306
<div class="row">
277307
<div class="col-md-12">
278308
<button id="conf_json_status_color" class="text-left btn btn-sm btn-square btn-block btn-success disabled">
@@ -311,14 +341,19 @@ <h4 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> osquery co
311341
<script src="/static/js/configuration.js"></script>
312342
<script type="text/javascript">
313343
$(document).ready(function() {
344+
var isReadOnly = {{ if $leftmeta.OsqueryValues.ReadOnly }}true{{ else }}false{{ end }};
345+
if (isReadOnly) {
346+
$('.main button').prop("disabled", true).addClass("disabled");
347+
}
348+
314349
// Codemirror editor for configuration
315350
// JSON validity check when content is changed
316351
var editorConfiguration = CodeMirror.fromTextArea(document.getElementById("final_conf"), {
317352
mode: 'application/json',
318353
lineNumbers: true,
319354
styleActiveLine: true,
320355
matchBrackets: true,
321-
readOnly: false
356+
readOnly: isReadOnly
322357
});
323358
$('#final_conf').data('CodeMirrorInstance', editorConfiguration);
324359
editorConfiguration.on('change', function(_editor){
@@ -361,7 +396,7 @@ <h4 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> osquery co
361396
lineNumbers: true,
362397
styleActiveLine: true,
363398
matchBrackets: true,
364-
readOnly: false
399+
readOnly: isReadOnly
365400
});
366401
$('#options_conf').data('CodeMirrorInstance', editorOptions);
367402
editorOptions.on('change', function(_editor){
@@ -407,7 +442,7 @@ <h4 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> osquery co
407442
lineNumbers: true,
408443
styleActiveLine: true,
409444
matchBrackets: true,
410-
readOnly: false
445+
readOnly: isReadOnly
411446
});
412447
$('#schedule_conf').data('CodeMirrorInstance', editorSchedule);
413448
editorSchedule.on('change', function(_editor){
@@ -452,7 +487,7 @@ <h4 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> osquery co
452487
lineNumbers: true,
453488
styleActiveLine: true,
454489
matchBrackets: true,
455-
readOnly: false
490+
readOnly: isReadOnly
456491
});
457492
$('#packs_conf').data('CodeMirrorInstance', editorPacks);
458493
editorPacks.on('change', function(_editor){
@@ -495,7 +530,7 @@ <h4 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> osquery co
495530
lineNumbers: true,
496531
styleActiveLine: true,
497532
matchBrackets: true,
498-
readOnly: false
533+
readOnly: isReadOnly
499534
});
500535
$('#atc_conf').data('CodeMirrorInstance', editorATC);
501536
editorATC.on('change', function(_editor){
@@ -538,7 +573,7 @@ <h4 class="alert-heading"><i class="fas fa-exclamation-triangle"></i> osquery co
538573
lineNumbers: true,
539574
styleActiveLine: true,
540575
matchBrackets: true,
541-
readOnly: false
576+
readOnly: isReadOnly
542577
});
543578
$('#decorators_conf').data('CodeMirrorInstance', editorDecorators);
544579
editorDecorators.on('change', function(_editor){

cmd/tls/handlers/handlers.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type HandlersTLS struct {
6060
Logs *logging.LoggerTLS
6161
WriteHandler *batchWriter
6262
OsqueryValues *config.YAMLConfigurationOsquery
63+
ConfigEndpoints *config.YAMLConfigurationEndpoints
6364
DebugHTTP *zerolog.Logger
6465
DebugHTTPConfig *config.YAMLConfigurationDebug
6566
}
@@ -149,6 +150,14 @@ func WithOsqueryValues(values *config.YAMLConfigurationOsquery) Option {
149150
}
150151
}
151152

153+
// WithConfigEndpoints to pass configuration endpoints values
154+
func WithConfigEndpoints(endpoints *config.YAMLConfigurationEndpoints) Option {
155+
return func(h *HandlersTLS) {
156+
h.ConfigEndpoints = endpoints
157+
}
158+
}
159+
160+
// WithDebugHTTP to pass debug HTTP configuration values
152161
func WithDebugHTTP(cfg *config.YAMLConfigurationDebug) Option {
153162
return func(h *HandlersTLS) {
154163
h.DebugHTTPConfig = cfg

cmd/tls/handlers/post.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package handlers
22

33
import (
4+
"bytes"
45
"compress/gzip"
56
"context"
7+
"crypto/sha256"
8+
"encoding/base64"
69
"encoding/json"
710
"fmt"
811
"io"
@@ -1063,3 +1066,124 @@ func (h *HandlersTLS) EnrollPackageHandler(w http.ResponseWriter, r *http.Reques
10631066
return
10641067
}
10651068
}
1069+
1070+
// OsqueryConfigEndpointHandler - Function to handle the osquery configuration endpoint
1071+
func (h *HandlersTLS) OsqueryConfigEndpointHandler(w http.ResponseWriter, r *http.Request) {
1072+
// Retrieve environment variable
1073+
envVar := r.PathValue("env")
1074+
if envVar == "" {
1075+
utils.HTTPResponse(w, "", http.StatusBadRequest, []byte(""))
1076+
return
1077+
}
1078+
// To prevent abuse, check if the received UUID is valid
1079+
if !utils.CheckUUID(envVar) {
1080+
utils.HTTPResponse(w, "", http.StatusBadRequest, []byte(""))
1081+
return
1082+
}
1083+
// Extract secret
1084+
secretVar := r.PathValue("secret")
1085+
if secretVar == "" {
1086+
utils.HTTPResponse(w, "", http.StatusBadRequest, []byte(""))
1087+
return
1088+
}
1089+
confirmed := false
1090+
integrityCheck := false
1091+
for _, confEndpoint := range *h.ConfigEndpoints {
1092+
if confEndpoint.Environment == envVar && confEndpoint.Secret == secretVar {
1093+
confirmed = true
1094+
integrityCheck = confEndpoint.IntegrityCheck
1095+
break
1096+
}
1097+
}
1098+
if !confirmed {
1099+
utils.HTTPResponse(w, "", http.StatusForbidden, []byte(""))
1100+
return
1101+
}
1102+
// If we are here, the secret is confirmed, so we can proceed to get the environment
1103+
env, err := h.Envs.GetByUUID(envVar)
1104+
if err != nil {
1105+
log.Err(err).Msg("error getting environment")
1106+
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
1107+
return
1108+
}
1109+
// Debug HTTP
1110+
if h.DebugHTTPConfig.EnableHTTP {
1111+
utils.DebugHTTPDump(h.DebugHTTP, r, h.DebugHTTPConfig.ShowBody)
1112+
}
1113+
// Decode read POST body
1114+
var o types.OsqueryConfigRequest
1115+
body, err := io.ReadAll(r.Body)
1116+
if err != nil {
1117+
log.Err(err).Msg("error reading POST body")
1118+
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
1119+
return
1120+
}
1121+
if err := json.Unmarshal(body, &o); err != nil {
1122+
log.Err(err).Msg("error parsing POST body")
1123+
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
1124+
return
1125+
}
1126+
// Decode base64 configuration
1127+
configDecoded, err := base64.StdEncoding.DecodeString(o.Configuration)
1128+
if err != nil {
1129+
log.Err(err).Msg("error decoding base64 configuration")
1130+
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
1131+
return
1132+
}
1133+
// Unzip configuration
1134+
gzipReader, err := gzip.NewReader(bytes.NewReader(configDecoded))
1135+
if err != nil {
1136+
log.Err(err).Msg("error decoding gzip configuration")
1137+
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
1138+
return
1139+
}
1140+
defer gzipReader.Close()
1141+
const maxConfigSize = 500 * 1024
1142+
limitedReader := io.LimitReader(gzipReader, maxConfigSize+1)
1143+
configuration, err := io.ReadAll(limitedReader)
1144+
if err != nil {
1145+
log.Err(err).Msg("error reading unzipped configuration")
1146+
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
1147+
return
1148+
}
1149+
if len(configuration) > maxConfigSize {
1150+
log.Error().Msg("unzipped configuration is larger than 500KB")
1151+
utils.HTTPResponse(w, "", http.StatusRequestEntityTooLarge, []byte(""))
1152+
return
1153+
}
1154+
// Verify integrity of the configuration using the provided hash
1155+
if integrityCheck {
1156+
hash := sha256.Sum256(configuration)
1157+
computedIntegrity := fmt.Sprintf("%x", hash)
1158+
if o.Integrity != computedIntegrity {
1159+
log.Warn().
1160+
Str("expected_integrity", o.Integrity).
1161+
Str("computed_integrity", computedIntegrity).
1162+
Msg("configuration integrity check failed")
1163+
utils.HTTPResponse(w, "", http.StatusBadRequest, []byte(""))
1164+
return
1165+
}
1166+
}
1167+
// Parse configuration
1168+
cnf, err := h.Envs.GenStructConf(configuration)
1169+
if err != nil {
1170+
log.Err(err).Msg("error parsing configuration")
1171+
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
1172+
return
1173+
}
1174+
// Update full configuration
1175+
if err := h.Envs.UpdateConfiguration(env.UUID, cnf); err != nil {
1176+
log.Err(err).Msg("error saving configuration")
1177+
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
1178+
return
1179+
}
1180+
// Update all configuration parts
1181+
if err := h.Envs.UpdateConfigurationParts(env.UUID, cnf); err != nil {
1182+
log.Err(err).Msg("error saving configuration parts")
1183+
utils.HTTPResponse(w, "", http.StatusInternalServerError, []byte(""))
1184+
return
1185+
}
1186+
response := TLSResponse{Message: "configuration saved successfully"}
1187+
// Send response
1188+
utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, response)
1189+
}

cmd/tls/main.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,15 @@ func loadYAMLConfiguration(file string) (config.TLSConfiguration, error) {
106106
func init() {
107107
// Initialize default flagParams
108108
flagParams = &config.ServiceParameters{
109-
Service: &config.YAMLConfigurationService{},
110-
DB: &config.YAMLConfigurationDB{},
111-
BatchWriter: &config.YAMLConfigurationWriter{},
112-
Redis: &config.YAMLConfigurationRedis{},
113-
Osquery: &config.YAMLConfigurationOsquery{},
114-
Osctrld: &config.YAMLConfigurationOsctrld{},
115-
Metrics: &config.YAMLConfigurationMetrics{},
116-
TLS: &config.YAMLConfigurationTLS{},
109+
Service: &config.YAMLConfigurationService{},
110+
DB: &config.YAMLConfigurationDB{},
111+
BatchWriter: &config.YAMLConfigurationWriter{},
112+
Redis: &config.YAMLConfigurationRedis{},
113+
Osquery: &config.YAMLConfigurationOsquery{},
114+
Osctrld: &config.YAMLConfigurationOsctrld{},
115+
ConfigEndpoints: &config.YAMLConfigurationEndpoints{},
116+
Metrics: &config.YAMLConfigurationMetrics{},
117+
TLS: &config.YAMLConfigurationTLS{},
117118
Logger: &config.YAMLConfigurationLogger{
118119
DB: &config.YAMLConfigurationDB{},
119120
S3: &config.S3Logger{},
@@ -283,6 +284,7 @@ func osctrlService() {
283284
handlers.WithLogs(loggerTLS),
284285
handlers.WithWriteHandler(tlsWriter),
285286
handlers.WithOsqueryValues(flagParams.Osquery),
287+
handlers.WithConfigEndpoints(flagParams.ConfigEndpoints),
286288
handlers.WithDebugHTTP(flagParams.Debug),
287289
)
288290
// ///////////////////////// ALL CONTENT IS UNAUTHENTICATED FOR TLS
@@ -330,6 +332,12 @@ func osctrlService() {
330332
muxTLS.HandleFunc("POST /{env}/{action}/{platform}/"+environments.DefaultScriptPath, handlersTLS.ScriptHandler)
331333
}
332334

335+
// Enable configuration endpoints if passed via YAML configuration
336+
if flagParams.ConfigEndpoints != nil && len(*flagParams.ConfigEndpoints) > 0 {
337+
log.Info().Msgf("Enabling %d configuration endpoints", len(*flagParams.ConfigEndpoints))
338+
muxTLS.HandleFunc("POST /{env}/{secret}/"+environments.DefaultConfigEndpointPath, handlersTLS.OsqueryConfigEndpointHandler)
339+
}
340+
333341
// ////////////////////////////// Everything is ready at this point!
334342
serviceListener := flagParams.Service.Listener + ":" + strconv.Itoa(flagParams.Service.Port)
335343
if flagParams.TLS.Termination {

cmd/tls/utils.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func loadedYAMLToServiceParams(yml config.TLSConfiguration, loadedFile string) *
6969
BatchWriter: &yml.BatchWriter,
7070
Redis: &yml.Redis,
7171
Osquery: &yml.Osquery,
72+
ConfigEndpoints: &yml.ConfigEndpoints,
7273
Osctrld: &yml.Osctrld,
7374
Metrics: &yml.Metrics,
7475
TLS: &yml.TLS,

0 commit comments

Comments
 (0)