Skip to content

Commit

Permalink
Added option in config wizard to set the cert expiration (#3945)
Browse files Browse the repository at this point in the history
Also automatically reissue certificates on startup if possible. This
stops cert expiry from being a hard error in deployments that include
the CA private key in the config file.
  • Loading branch information
scudette authored Dec 6, 2024
1 parent 4a41ec1 commit 8860089
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 15 deletions.
2 changes: 1 addition & 1 deletion artifacts/testdata/server/testcases/binary_blobs.out.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ SELECT * FROM switch( b={SELECT Complete FROM execve(argv=["rm", "-f", "/tmp/aut
"version": "",
"materialize": false,
"artifact": "",
"filestore_path": "351b4f6d59a4266cc7a2eab9cedf959eb6a4c924746044e6edeabdd1a477643e",
"filestore_path": "8adb952c7ee4388178e325b1a091c4a9e20b7e5f8436260c190df1c7c2f4a5f7",
"serve_url": "https://storage.googleapis.com/go.velocidex.com/winpmem_v3.3.rc3.exe",
"serve_path": "",
"filename": "winpmem_v3.3.rc3.exe",
Expand Down
2 changes: 1 addition & 1 deletion artifacts/testdata/server/testcases/hunts.out.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ SELECT * FROM hunt_results(hunt_id='H.49ba8939', artifact='Windows.Network.Netst
"Timestamp": "2020-08-01T09:17:02Z",
"FlowId": "F.BSJMEJIPT6P9I",
"ClientId": "C.4f5e52adf0a337a9",
"_OrgId": "",
"_OrgId": "root",
"Fqdn": "DESKTOP-BP4S7TF"
}
]SELECT hunt_id, create_time FROM hunts(hunt_id="H.49ba8939")[
Expand Down
16 changes: 8 additions & 8 deletions artifacts/testdata/server/testcases/tools.out.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ SELECT inventory_add(tool='Autorun_amd64', url='https://storage.googleapis.com/g
"name": "Autorun_amd64",
"url": "https://storage.googleapis.com/go.velocidex.com/autorunsc.exe",
"admin_override": true,
"filestore_path": "affbb298885d99a7569ef9ec463bbfbf9db112a11cec65cf78ec8657ab78950a",
"filestore_path": "c6bc3feacddc72332f5739fa1023515b6490daeec95711566601e01986eadfc1",
"serve_url": "https://storage.googleapis.com/go.velocidex.com/autorunsc.exe",
"filename": "autorunsc_x64.exe",
"hash": "083d7eee4ed40a3e5a35675503b0b6be0cb627b4cb1009d185a558a805f64153",
Expand Down Expand Up @@ -77,14 +77,14 @@ SELECT inventory_add(tool='Autorun_amd64', url='https://storage.googleapis.com/g
"inventory_get(tool='Autorun_amd64')": {
"Tool_Autorun_amd64_HASH": "083d7eee4ed40a3e5a35675503b0b6be0cb627b4cb1009d185a558a805f64153",
"Tool_Autorun_amd64_FILENAME": "autorunsc_x64.exe",
"Tool_Autorun_amd64_URL": "https://127.0.0.1:8/public/affbb298885d99a7569ef9ec463bbfbf9db112a11cec65cf78ec8657ab78950a",
"Tool_Autorun_amd64_URL": "https://127.0.0.1:8/public/c6bc3feacddc72332f5739fa1023515b6490daeec95711566601e01986eadfc1",
"Definition": {
"name": "Autorun_amd64",
"url": "https://storage.googleapis.com/go.velocidex.com/autorunsc.exe",
"serve_locally": true,
"admin_override": true,
"filestore_path": "affbb298885d99a7569ef9ec463bbfbf9db112a11cec65cf78ec8657ab78950a",
"serve_url": "https://127.0.0.1:8/public/affbb298885d99a7569ef9ec463bbfbf9db112a11cec65cf78ec8657ab78950a",
"filestore_path": "c6bc3feacddc72332f5739fa1023515b6490daeec95711566601e01986eadfc1",
"serve_url": "https://127.0.0.1:8/public/c6bc3feacddc72332f5739fa1023515b6490daeec95711566601e01986eadfc1",
"filename": "autorunsc_x64.exe",
"hash": "083d7eee4ed40a3e5a35675503b0b6be0cb627b4cb1009d185a558a805f64153",
"versions": [
Expand Down Expand Up @@ -116,7 +116,7 @@ SELECT inventory_add(tool='Autorun_amd64', url='https://storage.googleapis.com/g
"version": "",
"materialize": false,
"artifact": "",
"filestore_path": "1c21ee4d8609f81482dc0a78c641e4586488a9fd562ee28eec25e448a9d0b2e1",
"filestore_path": "77e71a25fc41db4cc20410a71b3d000b25d2bd0688f08b832e150e850078235c",
"serve_url": "",
"serve_path": "",
"filename": "yara_test.txt",
Expand All @@ -130,13 +130,13 @@ SELECT inventory_add(tool='Autorun_amd64', url='https://storage.googleapis.com/g
"inventory_get(tool=\"FooBar\")": {
"Tool_FooBar_HASH": "f03278c10a41adcc97f24a612a680e7aa43efb461b337fef3d2a3d47b51e77bb",
"Tool_FooBar_FILENAME": "yara_test.txt",
"Tool_FooBar_URL": "https://127.0.0.1:8/public/1c21ee4d8609f81482dc0a78c641e4586488a9fd562ee28eec25e448a9d0b2e1",
"Tool_FooBar_URL": "https://127.0.0.1:8/public/77e71a25fc41db4cc20410a71b3d000b25d2bd0688f08b832e150e850078235c",
"Definition": {
"name": "FooBar",
"serve_locally": true,
"admin_override": true,
"filestore_path": "1c21ee4d8609f81482dc0a78c641e4586488a9fd562ee28eec25e448a9d0b2e1",
"serve_url": "https://127.0.0.1:8/public/1c21ee4d8609f81482dc0a78c641e4586488a9fd562ee28eec25e448a9d0b2e1",
"filestore_path": "77e71a25fc41db4cc20410a71b3d000b25d2bd0688f08b832e150e850078235c",
"serve_url": "https://127.0.0.1:8/public/77e71a25fc41db4cc20410a71b3d000b25d2bd0688f08b832e150e850078235c",
"filename": "yara_test.txt",
"hash": "f03278c10a41adcc97f24a612a680e7aa43efb461b337fef3d2a3d47b51e77bb"
}
Expand Down
8 changes: 4 additions & 4 deletions artifacts/testdata/server/testcases/users.out.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ SELECT whoami() FROM scope()[
{
"user(user=\"TestUser\")": {
"name": "TestUser",
"org_id": "",
"org_name": "",
"org_id": "root",
"org_name": "\u003croot\u003e",
"picture": "",
"email": false,
"roles": [
Expand All @@ -39,8 +39,8 @@ SELECT whoami() FROM scope()[
{
"user(user=\"TestUser\")": {
"name": "TestUser",
"org_id": "",
"org_name": "",
"org_id": "root",
"org_name": "\u003croot\u003e",
"picture": "",
"email": false,
"roles": [
Expand Down
32 changes: 32 additions & 0 deletions bin/config_interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ portable than plain HTTP. Be sure to test this in your environment.
Default: tempfile.GetTempDir(),
}

expiry_question = &survey.Select{
Message: `Would you like to extend certificate expiration?
By defaults internal certificates are issued for 1 year.
If you expect this deployment to exist part one year you might
consider extending the default validation.
`,
Default: "1 Year",
Options: []string{"1 Year", "5 Years", "10 Years"},
}

output_question = &survey.Input{
Message: "Where should I write the server config file?",
Default: "server.config.yaml",
Expand Down Expand Up @@ -284,6 +296,26 @@ func doGenerateConfigInteractive() error {
return fmt.Errorf("Add users: %w", err)
}

expiration := ""
err = survey.AskOne(expiry_question,
&expiration, survey.WithValidator(survey.Required))
if err != nil {
return err
}

if config_obj.Defaults == nil {
config_obj.Defaults = &config_proto.Defaults{}
}

switch expiration {
case "1 Year":
config_obj.Defaults.CertificateValidityDays = 365
case "5 Years":
config_obj.Defaults.CertificateValidityDays = 365 * 5
case "10 Years":
config_obj.Defaults.CertificateValidityDays = 365 * 10
}

logger := logging.GetLogger(config_obj, &logging.ToolComponent)
logger.Info("Generating keys please wait....")
err = generateNewKeys(config_obj)
Expand Down
7 changes: 6 additions & 1 deletion services/orgs/orgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,12 @@ func (self *OrgManager) CreateNewOrg(name, id string) (
func (self *OrgManager) makeNewConfigObj(
record *api_proto.OrgRecord) *config_proto.Config {

result := proto.Clone(self.config_obj).(*config_proto.Config)
// The root org carries the real global config, but other orgs'
// config will be derived from the root org.
result := self.config_obj
if !utils.IsRootOrg(record.Id) {
result = proto.Clone(self.config_obj).(*config_proto.Config)
}

result.OrgId = utils.NormalizedOrgId(record.Id)
result.OrgName = record.Name
Expand Down
75 changes: 75 additions & 0 deletions services/sanity/certs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package sanity

import (
"errors"
"fmt"
"time"

config_proto "www.velocidex.com/golang/velociraptor/config/proto"
"www.velocidex.com/golang/velociraptor/crypto"
crypto_utils "www.velocidex.com/golang/velociraptor/crypto/utils"
"www.velocidex.com/golang/velociraptor/logging"
"www.velocidex.com/golang/velociraptor/utils"
)

// Check if the frontend certificates have expired. In the past this
// was a hard error but many users encountered it without warning and
// were confused about how to deal with it. Now we try to recover by
// automatically rekeying the certs on start up.

// In more secure deployments we recommend removing the CA private key
// from the server config, which prevents us from automatically
// rekeying the certificates. See
// https://docs.velociraptor.app/docs/deployment/security/ for
// relevant discussion.
func (self *SanityChecks) CheckCertificates(
config_obj *config_proto.Config) error {

cert, err := crypto_utils.ParseX509CertFromPemStr(
[]byte(config_obj.Frontend.Certificate))
if err != nil {
return err
}

now := utils.GetTime().Now()

if cert.NotBefore.After(now) || cert.NotAfter.Before(now) {
logger := logging.GetLogger(config_obj, &logging.FrontendComponent)
logger.Error("<red>Frontend Certificate is not valid</>: Certificate Valid NotBefore %v and Not After %v but Now is %v. See https://docs.velociraptor.app/knowledge_base/tips/rolling_certificates/",
cert.NotBefore.Format(time.RFC3339),
cert.NotAfter.Format(time.RFC3339),
now.Format(time.RFC3339),
)

if config_obj.CA != nil && config_obj.CA.PrivateKey != "" {
logger.Info("<green>Found CA private key in config</>, will automatically rotate keys, but you should consider updating the config file using `velociraptor config rotate`")

frontend_cert, err := crypto.GenerateServerCert(
config_obj, utils.GetSuperuserName(config_obj))
if err != nil {
return fmt.Errorf("Unable to create Frontend cert: %w", err)
}

config_obj.Frontend.Certificate = frontend_cert.Cert
config_obj.Frontend.PrivateKey = frontend_cert.PrivateKey

if config_obj.GUI != nil {
// Generate gRPC gateway certificate.
gw_certificate, err := crypto.GenerateServerCert(
config_obj, utils.GetGatewayName(config_obj))
if err != nil {
return err
}

config_obj.GUI.GwCertificate = gw_certificate.Cert
config_obj.GUI.GwPrivateKey = gw_certificate.PrivateKey
}

return nil
}

return errors.New("Certificate not valid")
}

return nil
}
6 changes: 6 additions & 0 deletions services/sanity/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ func (self *SanityChecks) CheckFrontendSettings(
logger := logging.GetLogger(config_obj, &logging.FrontendComponent)

if config_obj.Frontend != nil && config_obj.GUI != nil {
// Check that certificates are valid.
err := self.CheckCertificates(config_obj)
if err != nil {
return err
}

// Validate Allowed CIDRs
for _, cidr := range config_obj.GUI.AllowedCidr {
_, cidr_net, err := net.ParseCIDR(cidr)
Expand Down

0 comments on commit 8860089

Please sign in to comment.