mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
automatically renew macOS identity certificates 30 days prior to their expiration (#17057)
#15332 # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [x] Added/updated tests
This commit is contained in:
parent
12f519c853
commit
261332f76c
1
changes/15332-scep-renew
Normal file
1
changes/15332-scep-renew
Normal file
@ -0,0 +1 @@
|
|||||||
|
* Automatically renew macOS identity certificates for devices 30 days prior to their expiration.
|
@ -703,6 +703,7 @@ func newCleanupsAndAggregationSchedule(
|
|||||||
logger kitlog.Logger,
|
logger kitlog.Logger,
|
||||||
enrollHostLimiter fleet.EnrollHostLimiter,
|
enrollHostLimiter fleet.EnrollHostLimiter,
|
||||||
config *config.FleetConfig,
|
config *config.FleetConfig,
|
||||||
|
commander *apple_mdm.MDMAppleCommander,
|
||||||
) (*schedule.Schedule, error) {
|
) (*schedule.Schedule, error) {
|
||||||
const (
|
const (
|
||||||
name = string(fleet.CronCleanupsThenAggregation)
|
name = string(fleet.CronCleanupsThenAggregation)
|
||||||
@ -803,6 +804,12 @@ func newCleanupsAndAggregationSchedule(
|
|||||||
return verifyDiskEncryptionKeys(ctx, logger, ds, config)
|
return verifyDiskEncryptionKeys(ctx, logger, ds, config)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
schedule.WithJob(
|
||||||
|
"renew_scep_certificates",
|
||||||
|
func(ctx context.Context) error {
|
||||||
|
return service.RenewSCEPCertificates(ctx, logger, ds, config, commander)
|
||||||
|
},
|
||||||
|
),
|
||||||
schedule.WithJob("query_results_cleanup", func(ctx context.Context) error {
|
schedule.WithJob("query_results_cleanup", func(ctx context.Context) error {
|
||||||
config, err := ds.AppConfig(ctx)
|
config, err := ds.AppConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -681,7 +681,11 @@ the way that the Fleet server works.
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
|
if err := cronSchedules.StartCronSchedule(func() (fleet.CronSchedule, error) {
|
||||||
return newCleanupsAndAggregationSchedule(ctx, instanceID, ds, logger, redisWrapperDS, &config)
|
var commander *apple_mdm.MDMAppleCommander
|
||||||
|
if appCfg.MDM.EnabledAndConfigured {
|
||||||
|
commander = apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService)
|
||||||
|
}
|
||||||
|
return newCleanupsAndAggregationSchedule(ctx, instanceID, ds, logger, redisWrapperDS, &config, commander)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
initFatal(err, "failed to register cleanups_then_aggregations schedule")
|
initFatal(err, "failed to register cleanups_then_aggregations schedule")
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/cryptoutil"
|
||||||
nanodep_client "github.com/micromdm/nanodep/client"
|
nanodep_client "github.com/micromdm/nanodep/client"
|
||||||
"github.com/micromdm/nanodep/tokenpki"
|
"github.com/micromdm/nanodep/tokenpki"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
@ -594,6 +595,20 @@ func (m *MDMConfig) AppleAPNs() (cert *tls.Certificate, pemCert, pemKey []byte,
|
|||||||
return m.appleAPNs, m.appleAPNsPEMCert, m.appleAPNsPEMKey, nil
|
return m.appleAPNs, m.appleAPNsPEMCert, m.appleAPNsPEMKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MDMConfig) AppleAPNsTopic() (string, error) {
|
||||||
|
apnsCert, _, _, err := m.AppleAPNs()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parsing APNs certificates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mdmPushCertTopic, err := cryptoutil.TopicFromCert(apnsCert.Leaf)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("extracting topic from APNs certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mdmPushCertTopic, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AppleSCEP returns the parsed and validated TLS certificate for Apple SCEP.
|
// AppleSCEP returns the parsed and validated TLS certificate for Apple SCEP.
|
||||||
// It parses and validates it if it hasn't been done yet.
|
// It parses and validates it if it hasn't been done yet.
|
||||||
func (m *MDMConfig) AppleSCEP() (cert *tls.Certificate, pemCert, pemKey []byte, err error) {
|
func (m *MDMConfig) AppleSCEP() (cert *tls.Certificate, pemCert, pemKey []byte, err error) {
|
||||||
|
@ -953,3 +953,80 @@ func (ds *Datastore) MDMDeleteEULA(ctx context.Context, token string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ds *Datastore) GetHostCertAssociationsToExpire(ctx context.Context, expiryDays, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
||||||
|
// TODO(roberto): this is not good because we don't have any indexes on
|
||||||
|
// h.uuid, due to time constraints, I'm assuming that this
|
||||||
|
// function is called with a relatively low amount of shas
|
||||||
|
//
|
||||||
|
// Note that we use GROUP BY because we can't guarantee unique entries
|
||||||
|
// based on uuid in the hosts table.
|
||||||
|
stmt, args, err := sqlx.In(
|
||||||
|
`SELECT
|
||||||
|
h.uuid as host_uuid,
|
||||||
|
ncaa.sha256 as sha256,
|
||||||
|
COALESCE(MAX(hm.fleet_enroll_ref), '') as enroll_reference
|
||||||
|
FROM
|
||||||
|
nano_cert_auth_associations ncaa
|
||||||
|
LEFT JOIN hosts h ON h.uuid = ncaa.id
|
||||||
|
LEFT JOIN host_mdm hm ON hm.host_id = h.id
|
||||||
|
WHERE
|
||||||
|
cert_not_valid_after BETWEEN '0000-00-00' AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
||||||
|
AND renew_command_uuid IS NULL
|
||||||
|
GROUP BY
|
||||||
|
host_uuid, ncaa.sha256, cert_not_valid_after
|
||||||
|
ORDER BY cert_not_valid_after ASC
|
||||||
|
LIMIT ?
|
||||||
|
`, expiryDays, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ctxerr.Wrap(ctx, err, "building sqlx.In query")
|
||||||
|
}
|
||||||
|
|
||||||
|
var uuids []fleet.SCEPIdentityAssociation
|
||||||
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &uuids, stmt, args...); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, ctxerr.Wrap(ctx, err, "get identity certs close to expiry")
|
||||||
|
}
|
||||||
|
return uuids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ds *Datastore) SetCommandForPendingSCEPRenewal(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
||||||
|
if len(assocs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
args := make([]any, len(assocs)*3)
|
||||||
|
for i, assoc := range assocs {
|
||||||
|
sb.WriteString("(?, ?, ?),")
|
||||||
|
args[i*3] = assoc.HostUUID
|
||||||
|
args[i*3+1] = assoc.SHA256
|
||||||
|
args[i*3+2] = cmdUUID
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(`
|
||||||
|
INSERT INTO nano_cert_auth_associations (id, sha256, renew_command_uuid) VALUES %s
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
renew_command_uuid = VALUES(renew_command_uuid)
|
||||||
|
`, strings.TrimSuffix(sb.String(), ","))
|
||||||
|
|
||||||
|
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
|
||||||
|
res, err := tx.ExecContext(ctx, stmt, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update cert associations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: we can't use insertOnDuplicateDidInsert because the
|
||||||
|
// LastInsertId check only works tables that have an
|
||||||
|
// auto-incrementing primary key. See notes in that function
|
||||||
|
// and insertOnDuplicateDidUpdate to understand the mechanism.
|
||||||
|
affected, _ := res.RowsAffected()
|
||||||
|
if affected == 1 {
|
||||||
|
return errors.New("this function can only be used to update existing associations")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -2,19 +2,25 @@ package mysql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||||
mdm_types "github.com/fleetdm/fleet/v4/server/mdm"
|
mdm_types "github.com/fleetdm/fleet/v4/server/mdm"
|
||||||
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||||
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/service/certauth"
|
||||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||||
"github.com/fleetdm/fleet/v4/server/test"
|
"github.com/fleetdm/fleet/v4/server/test"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/micromdm/nanodep/tokenpki"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,6 +41,8 @@ func TestMDMShared(t *testing.T) {
|
|||||||
{"TestBatchSetProfileLabelAssociations", testBatchSetProfileLabelAssociations},
|
{"TestBatchSetProfileLabelAssociations", testBatchSetProfileLabelAssociations},
|
||||||
{"TestBatchSetProfilesTransactionError", testBatchSetMDMProfilesTransactionError},
|
{"TestBatchSetProfilesTransactionError", testBatchSetMDMProfilesTransactionError},
|
||||||
{"TestMDMEULA", testMDMEULA},
|
{"TestMDMEULA", testMDMEULA},
|
||||||
|
{"TestGetHostCertAssociationsToExpire", testSCEPRenewalHelpers},
|
||||||
|
{"TestSCEPRenewalHelpers", testSCEPRenewalHelpers},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@ -3130,3 +3138,133 @@ func testMDMEULA(t *testing.T, ds *Datastore) {
|
|||||||
err = ds.MDMInsertEULA(ctx, eula)
|
err = ds.MDMInsertEULA(ctx, eula)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testSCEPRenewalHelpers(t *testing.T, ds *Datastore) {
|
||||||
|
ctx := context.Background()
|
||||||
|
testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
|
||||||
|
testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
|
||||||
|
scepDepot, err := ds.NewSCEPDepot(testCertPEM, testKeyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
nanoStorage, err := ds.NewMDMAppleMDMStorage(testCertPEM, testKeyPEM)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var i int
|
||||||
|
setHost := func(notAfter time.Time) *fleet.Host {
|
||||||
|
i++
|
||||||
|
h, err := ds.NewHost(ctx, &fleet.Host{
|
||||||
|
Hostname: fmt.Sprintf("test-host%d-name", i),
|
||||||
|
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%d", i)),
|
||||||
|
NodeKey: ptr.String(fmt.Sprintf("nodekey-%d", i)),
|
||||||
|
UUID: fmt.Sprintf("test-uuid-%d", i),
|
||||||
|
Platform: "darwin",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// create a cert + association
|
||||||
|
serial, err := scepDepot.Serial()
|
||||||
|
require.NoError(t, err)
|
||||||
|
cert := &x509.Certificate{
|
||||||
|
SerialNumber: serial,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: "FleetDM Identity",
|
||||||
|
},
|
||||||
|
NotAfter: notAfter,
|
||||||
|
// use the host UUID, just to make sure they're
|
||||||
|
// different from each other, we don't care about the
|
||||||
|
// DER contents here
|
||||||
|
Raw: []byte(h.UUID)}
|
||||||
|
err = scepDepot.Put(cert.Subject.CommonName, cert)
|
||||||
|
require.NoError(t, err)
|
||||||
|
req := mdm.Request{
|
||||||
|
EnrollID: &mdm.EnrollID{ID: h.UUID},
|
||||||
|
Context: ctx,
|
||||||
|
}
|
||||||
|
certHash := certauth.HashCert(cert)
|
||||||
|
err = nanoStorage.AssociateCertHash(&req, certHash, notAfter)
|
||||||
|
require.NoError(t, err)
|
||||||
|
nanoEnroll(t, ds, h, false)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// certs expired at lest 1 year ago
|
||||||
|
h1 := setHost(time.Now().AddDate(-1, -1, 0))
|
||||||
|
h2 := setHost(time.Now().AddDate(-1, 0, 0))
|
||||||
|
// cert that expires in 1 month
|
||||||
|
h3 := setHost(time.Now().AddDate(0, 1, 0))
|
||||||
|
// cert that expires in 1 year
|
||||||
|
h4 := setHost(time.Now().AddDate(1, 0, 0))
|
||||||
|
|
||||||
|
// list assocs that expire in the next 10 days
|
||||||
|
assocs, err := ds.GetHostCertAssociationsToExpire(ctx, 10, 100)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, assocs, 2)
|
||||||
|
require.Equal(t, h1.UUID, assocs[0].HostUUID)
|
||||||
|
require.Equal(t, h2.UUID, assocs[1].HostUUID)
|
||||||
|
|
||||||
|
// list certs that expire in the next 1000 days with limit = 1
|
||||||
|
assocs, err = ds.GetHostCertAssociationsToExpire(ctx, 1000, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, assocs, 1)
|
||||||
|
require.Equal(t, h1.UUID, assocs[0].HostUUID)
|
||||||
|
|
||||||
|
// list certs that expire in the next 50 days
|
||||||
|
assocs, err = ds.GetHostCertAssociationsToExpire(ctx, 50, 100)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, assocs, 3)
|
||||||
|
require.Equal(t, h1.UUID, assocs[0].HostUUID)
|
||||||
|
require.Equal(t, h2.UUID, assocs[1].HostUUID)
|
||||||
|
require.Equal(t, h3.UUID, assocs[2].HostUUID)
|
||||||
|
|
||||||
|
// list certs that expire in the next 1000 days
|
||||||
|
assocs, err = ds.GetHostCertAssociationsToExpire(ctx, 1000, 100)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, assocs, 4)
|
||||||
|
require.Equal(t, h1.UUID, assocs[0].HostUUID)
|
||||||
|
require.Equal(t, h2.UUID, assocs[1].HostUUID)
|
||||||
|
require.Equal(t, h3.UUID, assocs[2].HostUUID)
|
||||||
|
require.Equal(t, h4.UUID, assocs[3].HostUUID)
|
||||||
|
|
||||||
|
checkSCEPRenew := func(assoc fleet.SCEPIdentityAssociation, want *string) {
|
||||||
|
var got *string
|
||||||
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||||
|
return sqlx.GetContext(ctx, q, &got, `SELECT renew_command_uuid FROM nano_cert_auth_associations WHERE id = ?`, assoc.HostUUID)
|
||||||
|
})
|
||||||
|
require.EqualValues(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert dummy nano commands
|
||||||
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||||
|
_, err = q.ExecContext(ctx, `
|
||||||
|
INSERT INTO nano_commands (command_uuid, request_type, command)
|
||||||
|
VALUES ('foo', 'foo', '<?xml'), ('bar', 'bar', '<?xml')
|
||||||
|
`)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
err = ds.SetCommandForPendingSCEPRenewal(ctx, []fleet.SCEPIdentityAssociation{}, "foo")
|
||||||
|
checkSCEPRenew(assocs[0], nil)
|
||||||
|
checkSCEPRenew(assocs[1], nil)
|
||||||
|
checkSCEPRenew(assocs[2], nil)
|
||||||
|
checkSCEPRenew(assocs[3], nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = ds.SetCommandForPendingSCEPRenewal(ctx, []fleet.SCEPIdentityAssociation{assocs[0]}, "foo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkSCEPRenew(assocs[0], ptr.String("foo"))
|
||||||
|
checkSCEPRenew(assocs[1], nil)
|
||||||
|
checkSCEPRenew(assocs[2], nil)
|
||||||
|
checkSCEPRenew(assocs[3], nil)
|
||||||
|
|
||||||
|
err = ds.SetCommandForPendingSCEPRenewal(ctx, assocs, "bar")
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkSCEPRenew(assocs[0], ptr.String("bar"))
|
||||||
|
checkSCEPRenew(assocs[1], ptr.String("bar"))
|
||||||
|
checkSCEPRenew(assocs[2], ptr.String("bar"))
|
||||||
|
checkSCEPRenew(assocs[3], ptr.String("bar"))
|
||||||
|
|
||||||
|
err = ds.SetCommandForPendingSCEPRenewal(ctx, []fleet.SCEPIdentityAssociation{{HostUUID: "foo", SHA256: "bar"}}, "bar")
|
||||||
|
require.ErrorContains(t, err, "this function can only be used to update existing associations")
|
||||||
|
}
|
||||||
|
@ -0,0 +1,143 @@
|
|||||||
|
package tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/jmoiron/sqlx/reflectx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
MigrationClient.AddMigration(Up_20240222073518, Down_20240222073518)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Up_20240222073518(tx *sql.Tx) error {
|
||||||
|
_, err := tx.Exec(`
|
||||||
|
ALTER TABLE nano_cert_auth_associations
|
||||||
|
-- used to detect identity certificates that are about to expire. While we have
|
||||||
|
-- access to the scep_certificates table, nanomdm assumes that you can use any CA
|
||||||
|
-- to issue your identity certificates, as such we can't add a foreign key here
|
||||||
|
-- without major changes.
|
||||||
|
ADD COLUMN cert_not_valid_after TIMESTAMP NULL,
|
||||||
|
-- used to track the command issued to renew the identity certificate (if one)
|
||||||
|
ADD COLUMN renew_command_uuid VARCHAR(127) COLLATE utf8mb4_unicode_ci NULL,
|
||||||
|
ADD CONSTRAINT renew_command_uuid_fk
|
||||||
|
FOREIGN KEY (renew_command_uuid) REFERENCES nano_commands (command_uuid)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to alter nano_cert_auth_associations table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := batchUpdateCertAssociationsTimestamps(tx); err != nil {
|
||||||
|
return fmt.Errorf("failed to update associations timestamps: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func batchUpdateCertAssociationsTimestamps(tx *sql.Tx) error {
|
||||||
|
txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)}
|
||||||
|
var totalCount int
|
||||||
|
if err := txx.Get(&totalCount, "SELECT COUNT(*) FROM scep_certificates"); err != nil {
|
||||||
|
return fmt.Errorf("failed to get total count of scep_certificates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchSize = 100
|
||||||
|
for offset := 0; offset < totalCount; offset += batchSize {
|
||||||
|
if err := updateCertAssociationTimestamps(&txx, batchSize, offset); err != nil {
|
||||||
|
return fmt.Errorf("updating batch with offset %d: %w", offset, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCertAssociationTimestamps(txx *sqlx.Tx, limit, offset int) error {
|
||||||
|
var scepCerts []struct {
|
||||||
|
Serial string `db:"serial"`
|
||||||
|
CertificatePEM []byte `db:"certificate_pem"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := txx.Select(
|
||||||
|
&scepCerts, `
|
||||||
|
SELECT certificate_pem
|
||||||
|
FROM scep_certificates
|
||||||
|
ORDER BY serial
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
`,
|
||||||
|
limit, offset,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve scep_certificates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shas := make([]string, len(scepCerts))
|
||||||
|
expiries := make(map[string]time.Time, len(scepCerts))
|
||||||
|
for i, rawCert := range scepCerts {
|
||||||
|
block, _ := pem.Decode(rawCert.CertificatePEM)
|
||||||
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to parse certificate with serial %s", rawCert.Serial)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hashed := hashCert(cert)
|
||||||
|
shas[i] = hashed
|
||||||
|
expiries[hashed] = cert.NotAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
var assocs []struct {
|
||||||
|
HostUUID string `db:"id"`
|
||||||
|
SHA256 string `db:"sha256"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
}
|
||||||
|
selectAssocStmt, selectAssocArgs, err := sqlx.In(`
|
||||||
|
SELECT id, sha256
|
||||||
|
FROM nano_cert_auth_associations
|
||||||
|
WHERE sha256 IN (?)
|
||||||
|
`, shas)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("building sqlx.In for cert associations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := txx.Select(&assocs, selectAssocStmt, selectAssocArgs...); err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve cert associations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
updateAssocArgs := make([]any, len(assocs)*3)
|
||||||
|
for i, assoc := range assocs {
|
||||||
|
sb.WriteString("(?, ?, ?),")
|
||||||
|
updateAssocArgs[i*3] = assoc.HostUUID
|
||||||
|
updateAssocArgs[i*3+1] = assoc.SHA256
|
||||||
|
updateAssocArgs[i*3+2] = expiries[assoc.SHA256]
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAssocStmt := fmt.Sprintf(`
|
||||||
|
INSERT INTO nano_cert_auth_associations (id, sha256, cert_not_valid_after) VALUES %s
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
cert_not_valid_after = VALUES(cert_not_valid_after),
|
||||||
|
updated_at = updated_at
|
||||||
|
`, strings.TrimSuffix(sb.String(), ","))
|
||||||
|
if _, err := txx.Exec(updateAssocStmt, updateAssocArgs...); err != nil {
|
||||||
|
return fmt.Errorf("failed to update cert associations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashCert(cert *x509.Certificate) string {
|
||||||
|
hashed := sha256.Sum256(cert.Raw)
|
||||||
|
return hex.EncodeToString(hashed[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func Down_20240222073518(tx *sql.Tx) error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
package tables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUp_20240222073518(t *testing.T) {
|
||||||
|
db := applyUpToPrev(t)
|
||||||
|
|
||||||
|
_, err := db.Exec("INSERT INTO scep_serials (serial) VALUES (1), (2)")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
threeDaysAgo := time.Now().UTC().Add(-72 * time.Hour).Truncate(time.Second)
|
||||||
|
_, err = db.Exec(`
|
||||||
|
INSERT INTO scep_certificates (serial, not_valid_before, not_valid_after, certificate_pem)
|
||||||
|
VALUES (?, ?, ?, ?), (?, ?, ?, ?)`,
|
||||||
|
// not_valid_* values don't really matter as the migration
|
||||||
|
// takes the value from the parsed cert.
|
||||||
|
1, threeDaysAgo, threeDaysAgo, dummyCert1,
|
||||||
|
2, threeDaysAgo, threeDaysAgo, dummyCert2,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sha1, sha2 := "4c51d40f56f5c5e13448995d4d2fd0b6b7befef860e4e7341c355ab38031ee35", "53c2dc9ce116a1df4adfba0c556843625fd1e91f83fc89a47c3267dff9a4c4ba" // #nosec G101
|
||||||
|
_, err = db.Exec(`
|
||||||
|
INSERT INTO nano_cert_auth_associations (id, sha256, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)`,
|
||||||
|
"uuid-1", sha1, threeDaysAgo, threeDaysAgo,
|
||||||
|
"uuid-2", sha2, threeDaysAgo, threeDaysAgo,
|
||||||
|
// host with duplicate cert, should never happen, but we don't
|
||||||
|
// have constraints in the db.
|
||||||
|
"uuid-3", sha2, threeDaysAgo, threeDaysAgo,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
applyNext(t, db)
|
||||||
|
|
||||||
|
var assoc struct {
|
||||||
|
HostUUID string `db:"id"`
|
||||||
|
SHA256 string `db:"sha256"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
CertNotValidAfter *time.Time `db:"cert_not_valid_after"`
|
||||||
|
RenewCommandUUID *string `db:"renew_command_uuid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
selectStmt := "SELECT id, sha256, created_at, updated_at, cert_not_valid_after, renew_command_uuid FROM nano_cert_auth_associations WHERE id = ?"
|
||||||
|
|
||||||
|
// new values are filled and timestamps preserved
|
||||||
|
err = sqlx.Get(db, &assoc, selectStmt, "uuid-1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "uuid-1", assoc.HostUUID)
|
||||||
|
require.Equal(t, sha1, assoc.SHA256)
|
||||||
|
require.Equal(t, threeDaysAgo, assoc.CreatedAt)
|
||||||
|
require.Equal(t, threeDaysAgo, assoc.UpdatedAt)
|
||||||
|
require.Equal(t, "2025-02-20 19:57:24", (*assoc.CertNotValidAfter).Format("2006-01-02 15:04:05"))
|
||||||
|
require.Nil(t, assoc.RenewCommandUUID)
|
||||||
|
|
||||||
|
err = sqlx.Get(db, &assoc, selectStmt, "uuid-2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "uuid-2", assoc.HostUUID)
|
||||||
|
require.Equal(t, sha2, assoc.SHA256)
|
||||||
|
require.Equal(t, threeDaysAgo, assoc.CreatedAt)
|
||||||
|
require.Equal(t, threeDaysAgo, assoc.UpdatedAt)
|
||||||
|
require.Equal(t, "2025-02-20 19:57:25", (*assoc.CertNotValidAfter).Format("2006-01-02 15:04:05"))
|
||||||
|
require.Nil(t, assoc.RenewCommandUUID)
|
||||||
|
|
||||||
|
err = sqlx.Get(db, &assoc, selectStmt, "uuid-3")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "uuid-3", assoc.HostUUID)
|
||||||
|
require.Equal(t, sha2, assoc.SHA256)
|
||||||
|
require.Equal(t, threeDaysAgo, assoc.CreatedAt)
|
||||||
|
require.Equal(t, threeDaysAgo, assoc.UpdatedAt)
|
||||||
|
require.Equal(t, "2025-02-20 19:57:25", (*assoc.CertNotValidAfter).Format("2006-01-02 15:04:05"))
|
||||||
|
require.Nil(t, assoc.RenewCommandUUID)
|
||||||
|
|
||||||
|
// creating a new association sets NULL as default values
|
||||||
|
_, err = db.Exec(`
|
||||||
|
INSERT INTO nano_cert_auth_associations (id, sha256)
|
||||||
|
VALUES (?, ?)`, "uuid-4", sha1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = sqlx.Get(db, &assoc, selectStmt, "uuid-4")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "uuid-4", assoc.HostUUID)
|
||||||
|
require.Equal(t, sha1, assoc.SHA256)
|
||||||
|
require.Nil(t, assoc.CertNotValidAfter)
|
||||||
|
require.Nil(t, assoc.RenewCommandUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dummyCert1 = []byte(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDgDCCAmigAwIBAgIBAjANBgkqhkiG9w0BAQsFADBBMQkwBwYDVQQGEwAxEDAO
|
||||||
|
BgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1NDRVAgQ0ExEDAOBgNVBAMTB0ZsZWV0
|
||||||
|
RE0wHhcNMjQwMjIxMTk0NzI0WhcNMjUwMjIwMTk1NzI0WjBdMRswGQYDVQQKExJm
|
||||||
|
bGVldC1vcmdhbml6YXRpb24xPjA8BgNVBAMTNWZsZWV0LXRlc3RkZXZpY2UtN0U0
|
||||||
|
RUJENTQtNjVDNy00RkU2LThFODUtOUUyNDEwMTQ1RENEMIIBIjANBgkqhkiG9w0B
|
||||||
|
AQEFAAOCAQ8AMIIBCgKCAQEAtgu75XAA5B2iys8DIZwdf2pdzFk157vyZZTnLI4r
|
||||||
|
7whAtLV556c6hjstyXhOmkut+kfiWHWoKQgbrtBj5LTfXwDbu11FapJvYPiI/GwD
|
||||||
|
vAQ+KbV9JcoGX70vL5Qmh+M2P+Ky//cE/zDc2YvPpEk4lcR+BNMJ1SnpRqZQ7ggC
|
||||||
|
0mw62TWbnOuQM4o+1ykvDpJBJhrLxdsEVNaGZVRb0W/GRLzMZNbkQtcBxhpi0yqy
|
||||||
|
iAScF75A0uy7pRSg1Fkr612qqA2bUcPMY901t264Hn7/YyAorVQS7iEvX9DVQbVu
|
||||||
|
T4GNtU5VaDrFsWBlDjVyj2+KUUU2g4klYJLEbfIjSa62WQIDAQABo2cwZTAOBgNV
|
||||||
|
HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHQYDVR0OBBYEFK9+cymm
|
||||||
|
EOOnA6EYicjk/OrJQI74MB8GA1UdIwQYMBaAFGmjhvWYNxfl4+HKLj8gWPCbXnqc
|
||||||
|
MA0GCSqGSIb3DQEBCwUAA4IBAQClD0xOhWS7Pqmiz6t0cC91sL2nAHgFtFhSQKNY
|
||||||
|
bQFGb0GIJQe0YVV1fJbDDqgHdaYXz+QwJWKfCui0ixYEPgho4SqdeNWsRgDs5EqU
|
||||||
|
chV6P/+yksXdKiu5f2wmf1T3oqgnrBxTm9bXe2ZQFR77FeeeA1AHUAOCESI3d6QF
|
||||||
|
ClvMWXXA/cutC3Wp/34M540trLGiM914whaf0Pb6Rx8HjldEn/dThWOKZDYK4MSK
|
||||||
|
4W2h3vw2aouSe46i86VtYTaDfTP5H4As+N6NunT7lK6sc3UWeWo7k3dliiywnQ4Y
|
||||||
|
AiCWE3wJMLpNwPaxdxz/grg8MLw9uPvfznZ9K9G/i5IDvI9O
|
||||||
|
-----END CERTIFICATE-----`)
|
||||||
|
|
||||||
|
var dummyCert2 = []byte(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDgDCCAmigAwIBAgIBAzANBgkqhkiG9w0BAQsFADBBMQkwBwYDVQQGEwAxEDAO
|
||||||
|
BgNVBAoTB3NjZXAtY2ExEDAOBgNVBAsTB1NDRVAgQ0ExEDAOBgNVBAMTB0ZsZWV0
|
||||||
|
RE0wHhcNMjQwMjIxMTk0NzI1WhcNMjUwMjIwMTk1NzI1WjBdMRswGQYDVQQKExJm
|
||||||
|
bGVldC1vcmdhbml6YXRpb24xPjA8BgNVBAMTNWZsZWV0LXRlc3RkZXZpY2UtMjQw
|
||||||
|
RkI4NEQtQzFBOS00ODhCLUEzNDItQkFBOTI2NTkwOEJBMIIBIjANBgkqhkiG9w0B
|
||||||
|
AQEFAAOCAQ8AMIIBCgKCAQEA0H/BTmCHrLrYHn0CWC+V0qMVvvOjE9fE178DOU8W
|
||||||
|
x/W5FGw9Vm+kYE2Tt/dQVLDYUnEg8u1v6JCN2YErGc3eLjyUPVz28778sVQCTc7s
|
||||||
|
Ax1QTxoRjxss7KDhSArdPyEu2YzbKfefEcVqPymDxQTeTKrscgN9XTIe6uvb6qCM
|
||||||
|
3HHKQJsUb8me8Sat8RyR1q+ahR7vrj9pHCXC/nyeK2l1xmnTgz2++C47zMVzjJ7g
|
||||||
|
VduG3SV440spcd/0TbCjYvu2qe4KcK1TypAbjyo/XOBI75ZV/S8uLmFR9C1XxDvQ
|
||||||
|
1rngNjyHa24LiweOYd3MIVe+g8htsOCOB8S9hWhN8Xn1OwIDAQABo2cwZTAOBgNV
|
||||||
|
HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHQYDVR0OBBYEFL3wSLk7
|
||||||
|
LWXNnzzNM4ZrIhPEL/0OMB8GA1UdIwQYMBaAFGmjhvWYNxfl4+HKLj8gWPCbXnqc
|
||||||
|
MA0GCSqGSIb3DQEBCwUAA4IBAQBXEbOh4hCbOfnRbtBUtd5s1aNd0N+E11eFJM6k
|
||||||
|
hwgOzHCgGrfG7eh/8QQ+4fYAnpyBEEz863EEqfmPY++MifLI7AI8b82EqxNVT8UK
|
||||||
|
YeFIvbtOwgKiq+YDLIzXPzRzOS6lgGB68nFNRyni4TeTCx5aaKBKfWlDNwOCdI7c
|
||||||
|
F97od8YqLp1wDG5caCKVvzLXbOvMZmdmjztKZoI+/SjPDpVsNKZrixYmijDVhZNf
|
||||||
|
Hd2ktxwNgxBx6TDAbCjwXhim2vPAg7ZoklxLHN4KS2F+ZtKDUbdR2WZyolxJh5QC
|
||||||
|
KuY7qFtlQZQFIcXnSpgXTC6tpG+oldTkz9exA4Zm5eXqTBfU
|
||||||
|
-----END CERTIFICATE-----`)
|
File diff suppressed because one or more lines are too long
@ -518,3 +518,20 @@ type ProfileMatcher interface {
|
|||||||
PreassignProfile(ctx context.Context, payload MDMApplePreassignProfilePayload) error
|
PreassignProfile(ctx context.Context, payload MDMApplePreassignProfilePayload) error
|
||||||
RetrieveProfiles(ctx context.Context, externalHostIdentifier string) (MDMApplePreassignHostProfiles, error)
|
RetrieveProfiles(ctx context.Context, externalHostIdentifier string) (MDMApplePreassignHostProfiles, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SCEPIdentityCertificate represents a certificate issued during MDM
|
||||||
|
// enrollment.
|
||||||
|
type SCEPIdentityCertificate struct {
|
||||||
|
Serial string `db:"serial"`
|
||||||
|
NotValidAfter time.Time `db:"not_valid_after"`
|
||||||
|
CertificatePEM []byte `db:"certificate_pem"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SCEPIdentityAssociation represents an association between an identity
|
||||||
|
// certificate an a specific host.
|
||||||
|
type SCEPIdentityAssociation struct {
|
||||||
|
HostUUID string `db:"host_uuid"`
|
||||||
|
SHA256 string `db:"sha256"`
|
||||||
|
EnrollReference string `db:"enroll_reference"`
|
||||||
|
RenewCommandUUID string `db:"renew_command_uuid"`
|
||||||
|
}
|
||||||
|
@ -777,6 +777,14 @@ type Datastore interface {
|
|||||||
|
|
||||||
SetDiskEncryptionResetStatus(ctx context.Context, hostID uint, status bool) error
|
SetDiskEncryptionResetStatus(ctx context.Context, hostID uint, status bool) error
|
||||||
|
|
||||||
|
// GetHostCertAssociationsToExpire retrieves host certificate
|
||||||
|
// associations that are close to expire and don't have a renewal in
|
||||||
|
// progress based on the provided arguments.
|
||||||
|
GetHostCertAssociationsToExpire(ctx context.Context, expiryDays, limit int) ([]SCEPIdentityAssociation, error)
|
||||||
|
|
||||||
|
// SetCommandForPendingSCEPRenewal tracks the command used to renew a scep certificate
|
||||||
|
SetCommandForPendingSCEPRenewal(ctx context.Context, assocs []SCEPIdentityAssociation, cmdUUID string) error
|
||||||
|
|
||||||
// UpdateVerificationHostMacOSProfiles updates status of macOS profiles installed on a given
|
// UpdateVerificationHostMacOSProfiles updates status of macOS profiles installed on a given
|
||||||
// host. The toVerify, toFail, and toRetry slices contain the identifiers of the profiles that
|
// host. The toVerify, toFail, and toRetry slices contain the identifiers of the profiles that
|
||||||
// should be verified, failed, and retried, respectively. For each profile in the toRetry slice,
|
// should be verified, failed, and retried, respectively. For each profile in the toRetry slice,
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
@ -14,6 +15,7 @@ import (
|
|||||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||||
"github.com/fleetdm/fleet/v4/server/logging"
|
"github.com/fleetdm/fleet/v4/server/logging"
|
||||||
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/internal/commonmdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/internal/commonmdm"
|
||||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||||
"github.com/go-kit/log/level"
|
"github.com/go-kit/log/level"
|
||||||
@ -741,6 +743,21 @@ func GenerateEnrollmentProfileMobileconfig(orgName, fleetURL, scepChallenge, top
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddEnrollmentRefToFleetURL(fleetURL, reference string) (string, error) {
|
||||||
|
if reference == "" {
|
||||||
|
return fleetURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(fleetURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parsing configured server URL: %w", err)
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
q.Add(mobileconfig.FleetEnrollReferenceKey, reference)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ProfileBimap implements bidirectional mapping for profiles, and utility
|
// ProfileBimap implements bidirectional mapping for profiles, and utility
|
||||||
// functions to generate those mappings based on frequently used operations.
|
// functions to generate those mappings based on frequently used operations.
|
||||||
type ProfileBimap struct {
|
type ProfileBimap struct {
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||||
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||||
"github.com/fleetdm/fleet/v4/server/mock"
|
"github.com/fleetdm/fleet/v4/server/mock"
|
||||||
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
|
nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep"
|
||||||
"github.com/go-kit/log"
|
"github.com/go-kit/log"
|
||||||
@ -133,6 +134,54 @@ func TestDEPService(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddEnrollmentRefToFleetURL(t *testing.T) {
|
||||||
|
const (
|
||||||
|
baseFleetURL = "https://example.com"
|
||||||
|
reference = "enroll-ref"
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fleetURL string
|
||||||
|
reference string
|
||||||
|
expectedOutput string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty Reference",
|
||||||
|
fleetURL: baseFleetURL,
|
||||||
|
reference: "",
|
||||||
|
expectedOutput: baseFleetURL,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid URL and Reference",
|
||||||
|
fleetURL: baseFleetURL,
|
||||||
|
reference: reference,
|
||||||
|
expectedOutput: baseFleetURL + "?" + mobileconfig.FleetEnrollReferenceKey + "=" + reference,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid URL",
|
||||||
|
fleetURL: "://invalid-url",
|
||||||
|
reference: reference,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
output, err := AddEnrollmentRefToFleetURL(tc.fleetURL, tc.reference)
|
||||||
|
if tc.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expectedOutput, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type notFoundError struct{}
|
type notFoundError struct{}
|
||||||
|
|
||||||
func (e notFoundError) IsNotFound() bool { return true }
|
func (e notFoundError) IsNotFound() bool { return true }
|
||||||
|
@ -97,7 +97,7 @@ func New(next service.CheckinAndCommandService, storage storage.CertAuthStore, o
|
|||||||
return certAuth
|
return certAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashCert(cert *x509.Certificate) string {
|
func HashCert(cert *x509.Certificate) string {
|
||||||
hashed := sha256.Sum256(cert.Raw)
|
hashed := sha256.Sum256(cert.Raw)
|
||||||
b := make([]byte, len(hashed))
|
b := make([]byte, len(hashed))
|
||||||
copy(b, hashed[:])
|
copy(b, hashed[:])
|
||||||
@ -112,7 +112,7 @@ func (s *CertAuth) associateNewEnrollment(r *mdm.Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger := ctxlog.Logger(r.Context, s.logger)
|
logger := ctxlog.Logger(r.Context, s.logger)
|
||||||
hash := hashCert(r.Certificate)
|
hash := HashCert(r.Certificate)
|
||||||
if hasHash, err := s.storage.HasCertHash(r, hash); err != nil {
|
if hasHash, err := s.storage.HasCertHash(r, hash); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if hasHash {
|
} else if hasHash {
|
||||||
@ -137,7 +137,7 @@ func (s *CertAuth) associateNewEnrollment(r *mdm.Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := s.storage.AssociateCertHash(r, hash); err != nil {
|
if err := s.storage.AssociateCertHash(r, hash, r.Certificate.NotAfter); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Info(
|
logger.Info(
|
||||||
@ -157,7 +157,7 @@ func (s *CertAuth) validateAssociateExistingEnrollment(r *mdm.Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger := ctxlog.Logger(r.Context, s.logger)
|
logger := ctxlog.Logger(r.Context, s.logger)
|
||||||
hash := hashCert(r.Certificate)
|
hash := HashCert(r.Certificate)
|
||||||
if isAssoc, err := s.storage.IsCertHashAssociated(r, hash); err != nil {
|
if isAssoc, err := s.storage.IsCertHashAssociated(r, hash); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if isAssoc {
|
} else if isAssoc {
|
||||||
@ -211,7 +211,7 @@ func (s *CertAuth) validateAssociateExistingEnrollment(r *mdm.Request) error {
|
|||||||
if s.warnOnly {
|
if s.warnOnly {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := s.storage.AssociateCertHash(r, hash); err != nil {
|
if err := s.storage.AssociateCertHash(r, hash, r.Certificate.NotAfter); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Info(
|
logger.Info(
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package allmulti
|
package allmulti
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/storage"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/storage"
|
||||||
)
|
)
|
||||||
@ -26,9 +28,9 @@ func (ms *MultiAllStorage) IsCertHashAssociated(r *mdm.Request, hash string) (bo
|
|||||||
return val.(bool), err
|
return val.(bool), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MultiAllStorage) AssociateCertHash(r *mdm.Request, hash string) error {
|
func (ms *MultiAllStorage) AssociateCertHash(r *mdm.Request, hash string, certNotValidAfter time.Time) error {
|
||||||
_, err := ms.execStores(r.Context, func(s storage.AllStorage) (interface{}, error) {
|
_, err := ms.execStores(r.Context, func(s storage.AllStorage) (interface{}, error) {
|
||||||
return nil, s.AssociateCertHash(r, hash)
|
return nil, s.AssociateCertHash(r, hash, certNotValidAfter)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||||
)
|
)
|
||||||
@ -52,7 +53,7 @@ func (s *FileStorage) IsCertHashAssociated(r *mdm.Request, hash string) (bool, e
|
|||||||
return strings.ToLower(string(b)) == strings.ToLower(hash), nil
|
return strings.ToLower(string(b)) == strings.ToLower(hash), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileStorage) AssociateCertHash(r *mdm.Request, hash string) error {
|
func (s *FileStorage) AssociateCertHash(r *mdm.Request, hash string, _ time.Time) error {
|
||||||
f, err := os.OpenFile(
|
f, err := os.OpenFile(
|
||||||
path.Join(s.path, CertAuthAssociationsFilename),
|
path.Join(s.path, CertAuthAssociationsFilename),
|
||||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
|
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
|
||||||
|
@ -3,6 +3,7 @@ package mysql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||||
)
|
)
|
||||||
@ -38,14 +39,18 @@ func (s *MySQLStorage) IsCertHashAssociated(r *mdm.Request, hash string) (bool,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MySQLStorage) AssociateCertHash(r *mdm.Request, hash string) error {
|
func (s *MySQLStorage) AssociateCertHash(r *mdm.Request, hash string, certNotValidAfter time.Time) error {
|
||||||
_, err := s.db.ExecContext(
|
_, err := s.db.ExecContext(
|
||||||
r.Context, `
|
r.Context, `
|
||||||
INSERT INTO nano_cert_auth_associations (id, sha256) VALUES (?, ?)
|
INSERT INTO nano_cert_auth_associations (id, sha256, cert_not_valid_after) VALUES (?, ?, ?)
|
||||||
ON DUPLICATE KEY
|
ON DUPLICATE KEY
|
||||||
UPDATE sha256 = VALUES(sha256);`,
|
UPDATE
|
||||||
|
sha256 = VALUES(sha256),
|
||||||
|
cert_not_valid_after = VALUES(cert_not_valid_after),
|
||||||
|
renew_command_uuid = NULL;`,
|
||||||
r.ID,
|
r.ID,
|
||||||
strings.ToLower(hash),
|
strings.ToLower(hash),
|
||||||
|
certNotValidAfter,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package pgsql
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||||
)
|
)
|
||||||
@ -39,7 +40,7 @@ func (s *PgSQLStorage) IsCertHashAssociated(r *mdm.Request, hash string) (bool,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AssociateCertHash "DO NOTHING" on duplicated keys
|
// AssociateCertHash "DO NOTHING" on duplicated keys
|
||||||
func (s *PgSQLStorage) AssociateCertHash(r *mdm.Request, hash string) error {
|
func (s *PgSQLStorage) AssociateCertHash(r *mdm.Request, hash string, _ time.Time) error {
|
||||||
_, err := s.db.ExecContext(
|
_, err := s.db.ExecContext(
|
||||||
r.Context, `
|
r.Context, `
|
||||||
INSERT INTO cert_auth_associations (id, sha256)
|
INSERT INTO cert_auth_associations (id, sha256)
|
||||||
|
@ -5,6 +5,7 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||||
)
|
)
|
||||||
@ -62,7 +63,7 @@ type CertAuthStore interface {
|
|||||||
HasCertHash(r *mdm.Request, hash string) (bool, error)
|
HasCertHash(r *mdm.Request, hash string) (bool, error)
|
||||||
EnrollmentHasCertHash(r *mdm.Request, hash string) (bool, error)
|
EnrollmentHasCertHash(r *mdm.Request, hash string) (bool, error)
|
||||||
IsCertHashAssociated(r *mdm.Request, hash string) (bool, error)
|
IsCertHashAssociated(r *mdm.Request, hash string) (bool, error)
|
||||||
AssociateCertHash(r *mdm.Request, hash string) error
|
AssociateCertHash(r *mdm.Request, hash string, certNotValidAfter time.Time) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoreMigrator retrieves MDM check-ins
|
// StoreMigrator retrieves MDM check-ins
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||||
@ -47,7 +48,7 @@ type EnrollmentHasCertHashFunc func(r *mdm.Request, hash string) (bool, error)
|
|||||||
|
|
||||||
type IsCertHashAssociatedFunc func(r *mdm.Request, hash string) (bool, error)
|
type IsCertHashAssociatedFunc func(r *mdm.Request, hash string) (bool, error)
|
||||||
|
|
||||||
type AssociateCertHashFunc func(r *mdm.Request, hash string) error
|
type AssociateCertHashFunc func(r *mdm.Request, hash string, certNotValidAfter time.Time) error
|
||||||
|
|
||||||
type RetrieveMigrationCheckinsFunc func(p0 context.Context, p1 chan<- interface{}) error
|
type RetrieveMigrationCheckinsFunc func(p0 context.Context, p1 chan<- interface{}) error
|
||||||
|
|
||||||
@ -241,11 +242,11 @@ func (fs *MDMAppleStore) IsCertHashAssociated(r *mdm.Request, hash string) (bool
|
|||||||
return fs.IsCertHashAssociatedFunc(r, hash)
|
return fs.IsCertHashAssociatedFunc(r, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *MDMAppleStore) AssociateCertHash(r *mdm.Request, hash string) error {
|
func (fs *MDMAppleStore) AssociateCertHash(r *mdm.Request, hash string, certNotValidAfter time.Time) error {
|
||||||
fs.mu.Lock()
|
fs.mu.Lock()
|
||||||
fs.AssociateCertHashFuncInvoked = true
|
fs.AssociateCertHashFuncInvoked = true
|
||||||
fs.mu.Unlock()
|
fs.mu.Unlock()
|
||||||
return fs.AssociateCertHashFunc(r, hash)
|
return fs.AssociateCertHashFunc(r, hash, certNotValidAfter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *MDMAppleStore) RetrieveMigrationCheckins(p0 context.Context, p1 chan<- interface{}) error {
|
func (fs *MDMAppleStore) RetrieveMigrationCheckins(p0 context.Context, p1 chan<- interface{}) error {
|
||||||
|
@ -544,6 +544,10 @@ type GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet
|
|||||||
|
|
||||||
type SetDiskEncryptionResetStatusFunc func(ctx context.Context, hostID uint, status bool) error
|
type SetDiskEncryptionResetStatusFunc func(ctx context.Context, hostID uint, status bool) error
|
||||||
|
|
||||||
|
type GetHostCertAssociationsToExpireFunc func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error)
|
||||||
|
|
||||||
|
type SetCommandForPendingSCEPRenewalFunc func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error
|
||||||
|
|
||||||
type UpdateHostMDMProfilesVerificationFunc func(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error
|
type UpdateHostMDMProfilesVerificationFunc func(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error
|
||||||
|
|
||||||
type GetHostMDMProfilesExpectedForVerificationFunc func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error)
|
type GetHostMDMProfilesExpectedForVerificationFunc func(ctx context.Context, host *fleet.Host) (map[string]*fleet.ExpectedMDMProfile, error)
|
||||||
@ -1610,6 +1614,12 @@ type DataStore struct {
|
|||||||
SetDiskEncryptionResetStatusFunc SetDiskEncryptionResetStatusFunc
|
SetDiskEncryptionResetStatusFunc SetDiskEncryptionResetStatusFunc
|
||||||
SetDiskEncryptionResetStatusFuncInvoked bool
|
SetDiskEncryptionResetStatusFuncInvoked bool
|
||||||
|
|
||||||
|
GetHostCertAssociationsToExpireFunc GetHostCertAssociationsToExpireFunc
|
||||||
|
GetHostCertAssociationsToExpireFuncInvoked bool
|
||||||
|
|
||||||
|
SetCommandForPendingSCEPRenewalFunc SetCommandForPendingSCEPRenewalFunc
|
||||||
|
SetCommandForPendingSCEPRenewalFuncInvoked bool
|
||||||
|
|
||||||
UpdateHostMDMProfilesVerificationFunc UpdateHostMDMProfilesVerificationFunc
|
UpdateHostMDMProfilesVerificationFunc UpdateHostMDMProfilesVerificationFunc
|
||||||
UpdateHostMDMProfilesVerificationFuncInvoked bool
|
UpdateHostMDMProfilesVerificationFuncInvoked bool
|
||||||
|
|
||||||
@ -3868,6 +3878,20 @@ func (s *DataStore) SetDiskEncryptionResetStatus(ctx context.Context, hostID uin
|
|||||||
return s.SetDiskEncryptionResetStatusFunc(ctx, hostID, status)
|
return s.SetDiskEncryptionResetStatusFunc(ctx, hostID, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DataStore) GetHostCertAssociationsToExpire(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.GetHostCertAssociationsToExpireFuncInvoked = true
|
||||||
|
s.mu.Unlock()
|
||||||
|
return s.GetHostCertAssociationsToExpireFunc(ctx, expiryDays, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DataStore) SetCommandForPendingSCEPRenewal(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.SetCommandForPendingSCEPRenewalFuncInvoked = true
|
||||||
|
s.mu.Unlock()
|
||||||
|
return s.SetCommandForPendingSCEPRenewalFunc(ctx, assocs, cmdUUID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DataStore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error {
|
func (s *DataStore) UpdateHostMDMProfilesVerification(ctx context.Context, host *fleet.Host, toVerify []string, toFail []string, toRetry []string) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.UpdateHostMDMProfilesVerificationFuncInvoked = true
|
s.UpdateHostMDMProfilesVerificationFuncInvoked = true
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -20,6 +19,7 @@ import (
|
|||||||
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
||||||
"github.com/fleetdm/fleet/v4/server"
|
"github.com/fleetdm/fleet/v4/server"
|
||||||
"github.com/fleetdm/fleet/v4/server/authz"
|
"github.com/fleetdm/fleet/v4/server/authz"
|
||||||
|
"github.com/fleetdm/fleet/v4/server/config"
|
||||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||||
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
"github.com/fleetdm/fleet/v4/server/contexts/license"
|
||||||
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
||||||
@ -32,8 +32,8 @@ import (
|
|||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||||
"github.com/fleetdm/fleet/v4/server/sso"
|
"github.com/fleetdm/fleet/v4/server/sso"
|
||||||
"github.com/fleetdm/fleet/v4/server/worker"
|
"github.com/fleetdm/fleet/v4/server/worker"
|
||||||
kitlog "github.com/go-kit/kit/log"
|
kitlog "github.com/go-kit/log"
|
||||||
"github.com/go-kit/kit/log/level"
|
"github.com/go-kit/log/level"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/groob/plist"
|
"github.com/groob/plist"
|
||||||
"github.com/micromdm/nanodep/godep"
|
"github.com/micromdm/nanodep/godep"
|
||||||
@ -1087,16 +1087,9 @@ func (svc *Service) GetMDMAppleEnrollmentProfileByToken(ctx context.Context, tok
|
|||||||
return nil, ctxerr.Wrap(ctx, err)
|
return nil, ctxerr.Wrap(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
enrollURL := appConfig.ServerSettings.ServerURL
|
enrollURL, err := apple_mdm.AddEnrollmentRefToFleetURL(appConfig.ServerSettings.ServerURL, ref)
|
||||||
if ref != "" {
|
if err != nil {
|
||||||
u, err := url.Parse(enrollURL)
|
return nil, ctxerr.Wrap(ctx, err, "adding reference to fleet URL")
|
||||||
if err != nil {
|
|
||||||
return nil, ctxerr.Wrap(ctx, err, "parsing configured server URL")
|
|
||||||
}
|
|
||||||
q := u.Query()
|
|
||||||
q.Add(mobileconfig.FleetEnrollReferenceKey, ref)
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
enrollURL = u.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mobileconfig, err := apple_mdm.GenerateEnrollmentProfileMobileconfig(
|
mobileconfig, err := apple_mdm.GenerateEnrollmentProfileMobileconfig(
|
||||||
@ -2824,3 +2817,123 @@ func (svc *Service) getConfigAppleBMDefaultTeamID(ctx context.Context, appCfg *f
|
|||||||
|
|
||||||
return tmID, nil
|
return tmID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// scepCertRenewalThresholdDays defines the number of days before a SCEP
|
||||||
|
// certificate must be renewed.
|
||||||
|
const scepCertRenewalThresholdDays = 30
|
||||||
|
|
||||||
|
// maxCertsRenewalPerRun specifies the maximum number of certificates to renew
|
||||||
|
// in a single cron run.
|
||||||
|
//
|
||||||
|
// Assuming that the cron runs every hour, we'll enqueue 24,000 renewals per
|
||||||
|
// day, and we have room for 24,000 * scepCertRenewalThresholdDays total
|
||||||
|
// renewals.
|
||||||
|
//
|
||||||
|
// For a default of 30 days as a threshold this gives us room for a fleet of
|
||||||
|
// 720,000 devices expiring at the same time.
|
||||||
|
const maxCertsRenewalPerRun = 100
|
||||||
|
|
||||||
|
func RenewSCEPCertificates(
|
||||||
|
ctx context.Context,
|
||||||
|
logger kitlog.Logger,
|
||||||
|
ds fleet.Datastore,
|
||||||
|
config *config.FleetConfig,
|
||||||
|
commander *apple_mdm.MDMAppleCommander,
|
||||||
|
) error {
|
||||||
|
if !config.MDM.IsAppleSCEPSet() {
|
||||||
|
logger.Log("inf", "skipping renewal of macOS SCEP certificates as MDM is not fully configured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if commander == nil {
|
||||||
|
logger.Log("inf", "skipping renewal of macOS SCEP certificates as apple_mdm.MDMAppleCommander was not provided")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each hash, grab the host that uses it as its identity certificate
|
||||||
|
certAssociations, err := ds.GetHostCertAssociationsToExpire(ctx, scepCertRenewalThresholdDays, maxCertsRenewalPerRun)
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "getting host cert associations")
|
||||||
|
}
|
||||||
|
|
||||||
|
appConfig, err := ds.AppConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "getting AppConfig")
|
||||||
|
}
|
||||||
|
|
||||||
|
mdmPushCertTopic, err := config.MDM.AppleAPNsTopic()
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "getting certificate topic")
|
||||||
|
}
|
||||||
|
|
||||||
|
// assocsWithRefs stores hosts that have enrollment references on their
|
||||||
|
// enrollment profiles. This is the case for ADE-enrolled hosts using
|
||||||
|
// SSO to authenticate.
|
||||||
|
assocsWithRefs := []fleet.SCEPIdentityAssociation{}
|
||||||
|
// assocsWithoutRefs stores hosts that don't have an enrollment
|
||||||
|
// reference in their enrollment profile.
|
||||||
|
assocsWithoutRefs := []fleet.SCEPIdentityAssociation{}
|
||||||
|
for _, assoc := range certAssociations {
|
||||||
|
if assoc.EnrollReference != "" {
|
||||||
|
assocsWithRefs = append(assocsWithRefs, assoc)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assocsWithoutRefs = append(assocsWithoutRefs, assoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a single command for all the hosts without references.
|
||||||
|
if len(assocsWithoutRefs) > 0 {
|
||||||
|
profile, err := apple_mdm.GenerateEnrollmentProfileMobileconfig(
|
||||||
|
appConfig.OrgInfo.OrgName,
|
||||||
|
appConfig.ServerSettings.ServerURL,
|
||||||
|
config.MDM.AppleSCEPChallenge,
|
||||||
|
mdmPushCertTopic,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "generating enrollment profile for hosts without enroll reference")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdUUID := uuid.NewString()
|
||||||
|
var uuids []string
|
||||||
|
for _, assoc := range assocsWithoutRefs {
|
||||||
|
uuids = append(uuids, assoc.HostUUID)
|
||||||
|
assoc.RenewCommandUUID = cmdUUID
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commander.InstallProfile(ctx, uuids, profile, cmdUUID); err != nil {
|
||||||
|
return ctxerr.Wrapf(ctx, err, "sending InstallProfile command for hosts %s", assocsWithoutRefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ds.SetCommandForPendingSCEPRenewal(ctx, assocsWithoutRefs, cmdUUID); err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "setting pending command associations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send individual commands for each host with a reference
|
||||||
|
for _, assoc := range assocsWithRefs {
|
||||||
|
enrollURL, err := apple_mdm.AddEnrollmentRefToFleetURL(appConfig.ServerSettings.ServerURL, assoc.EnrollReference)
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "adding reference to fleet URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := apple_mdm.GenerateEnrollmentProfileMobileconfig(
|
||||||
|
appConfig.OrgInfo.OrgName,
|
||||||
|
enrollURL,
|
||||||
|
config.MDM.AppleSCEPChallenge,
|
||||||
|
mdmPushCertTopic,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "generating enrollment profile for hosts with enroll reference")
|
||||||
|
}
|
||||||
|
cmdUUID := uuid.NewString()
|
||||||
|
if err := commander.InstallProfile(ctx, []string{assoc.HostUUID}, profile, cmdUUID); err != nil {
|
||||||
|
return ctxerr.Wrapf(ctx, err, "sending InstallProfile command for hosts %s", assocsWithRefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ds.SetCommandForPendingSCEPRenewal(ctx, []fleet.SCEPIdentityAssociation{assoc}, cmdUUID); err != nil {
|
||||||
|
return ctxerr.Wrap(ctx, err, "setting pending command associations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -3,17 +3,25 @@ package service
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fleetdm/fleet/v4/server/authz"
|
"github.com/fleetdm/fleet/v4/server/authz"
|
||||||
"github.com/fleetdm/fleet/v4/server/config"
|
"github.com/fleetdm/fleet/v4/server/config"
|
||||||
@ -23,6 +31,7 @@ import (
|
|||||||
fleetmdm "github.com/fleetdm/fleet/v4/server/mdm"
|
fleetmdm "github.com/fleetdm/fleet/v4/server/mdm"
|
||||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
|
||||||
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/log/stdlogfmt"
|
||||||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
||||||
nanomdm_pushsvc "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/service"
|
nanomdm_pushsvc "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/service"
|
||||||
"github.com/fleetdm/fleet/v4/server/mock"
|
"github.com/fleetdm/fleet/v4/server/mock"
|
||||||
@ -2656,3 +2665,248 @@ func mobileconfigForTestWithContent(outerName, outerIdentifier, innerIdentifier,
|
|||||||
</plist>
|
</plist>
|
||||||
`, innerName, innerIdentifier, innerType, outerName, outerIdentifier, uuid.New().String()))
|
`, innerName, innerIdentifier, innerType, outerName, outerIdentifier, uuid.New().String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateCertWithAPNsTopic() ([]byte, []byte, error) {
|
||||||
|
// generate a new private key
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set up the OID for UID
|
||||||
|
oidUID := asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1}
|
||||||
|
|
||||||
|
// set up a certificate template with the required UID in the Subject
|
||||||
|
notBefore := time.Now()
|
||||||
|
notAfter := notBefore.Add(365 * 24 * time.Hour)
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
ExtraNames: []pkix.AttributeTypeAndValue{
|
||||||
|
{
|
||||||
|
Type: oidUID,
|
||||||
|
Value: "com.apple.mgmt.Example",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NotBefore: notBefore,
|
||||||
|
NotAfter: notAfter,
|
||||||
|
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a self-signed certificate
|
||||||
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode to PEM
|
||||||
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||||
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||||
|
|
||||||
|
return certPEM, keyPEM, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTest(t *testing.T) (context.Context, kitlog.Logger, *mock.Store, *config.FleetConfig, *mock.MDMAppleStore, *apple_mdm.MDMAppleCommander) {
|
||||||
|
ctx := context.Background()
|
||||||
|
logger := kitlog.NewNopLogger()
|
||||||
|
cfg := config.TestConfig()
|
||||||
|
testCertPEM, testKeyPEM, err := generateCertWithAPNsTopic()
|
||||||
|
require.NoError(t, err)
|
||||||
|
config.SetTestMDMConfig(t, &cfg, testCertPEM, testKeyPEM, testBMToken, "../../server/service/testdata")
|
||||||
|
ds := new(mock.Store)
|
||||||
|
mdmStorage := &mock.MDMAppleStore{}
|
||||||
|
pushFactory, _ := newMockAPNSPushProviderFactory()
|
||||||
|
pusher := nanomdm_pushsvc.New(
|
||||||
|
mdmStorage,
|
||||||
|
mdmStorage,
|
||||||
|
pushFactory,
|
||||||
|
stdlogfmt.New(),
|
||||||
|
)
|
||||||
|
commander := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher)
|
||||||
|
|
||||||
|
return ctx, logger, ds, &cfg, mdmStorage, commander
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenewSCEPCertificatesMDMConfigNotSet(t *testing.T) {
|
||||||
|
ctx, logger, ds, cfg, _, commander := setupTest(t)
|
||||||
|
cfg.MDM = config.MDMConfig{} // ensure MDM is not fully configured
|
||||||
|
err := RenewSCEPCertificates(ctx, logger, ds, cfg, commander)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenewSCEPCertificatesCommanderNil(t *testing.T) {
|
||||||
|
ctx, logger, ds, cfg, _, _ := setupTest(t)
|
||||||
|
err := RenewSCEPCertificates(ctx, logger, ds, cfg, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenewSCEPCertificatesBranches(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
customExpectations func(*testing.T, *mock.Store, *config.FleetConfig, *mock.MDMAppleStore, *apple_mdm.MDMAppleCommander)
|
||||||
|
expectedError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No Certs to Renew",
|
||||||
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
||||||
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GetHostCertAssociationsToExpire Errors",
|
||||||
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
||||||
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
||||||
|
return nil, errors.New("database error")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AppConfig Errors",
|
||||||
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
||||||
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||||
|
return nil, errors.New("app config error")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InstallProfile for hostsWithoutRefs",
|
||||||
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
||||||
|
var wantCommandUUID string
|
||||||
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
||||||
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID1", EnrollReference: ""}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
||||||
|
require.Equal(t, "InstallProfile", cmd.Command.RequestType)
|
||||||
|
wantCommandUUID = cmd.CommandUUID
|
||||||
|
return map[string]error{}, nil
|
||||||
|
}
|
||||||
|
ds.SetCommandForPendingSCEPRenewalFunc = func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
||||||
|
require.Len(t, assocs, 1)
|
||||||
|
require.Equal(t, "hostUUID1", assocs[0].HostUUID)
|
||||||
|
require.Equal(t, cmdUUID, wantCommandUUID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.True(t, appleStore.EnqueueCommandFuncInvoked)
|
||||||
|
require.True(t, ds.SetCommandForPendingSCEPRenewalFuncInvoked)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InstallProfile for hostsWithoutRefs fails",
|
||||||
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
||||||
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
||||||
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID1", EnrollReference: ""}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
||||||
|
return map[string]error{}, errors.New("foo")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InstallProfile for hostsWithRefs",
|
||||||
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
||||||
|
var wantCommandUUID string
|
||||||
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
||||||
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID2", EnrollReference: "ref1"}}, nil
|
||||||
|
}
|
||||||
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
||||||
|
require.Equal(t, "InstallProfile", cmd.Command.RequestType)
|
||||||
|
wantCommandUUID = cmd.CommandUUID
|
||||||
|
return map[string]error{}, nil
|
||||||
|
}
|
||||||
|
ds.SetCommandForPendingSCEPRenewalFunc = func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
||||||
|
require.Len(t, assocs, 1)
|
||||||
|
require.Equal(t, "hostUUID2", assocs[0].HostUUID)
|
||||||
|
require.Equal(t, cmdUUID, wantCommandUUID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.True(t, appleStore.EnqueueCommandFuncInvoked)
|
||||||
|
require.True(t, ds.SetCommandForPendingSCEPRenewalFuncInvoked)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
expectedError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InstallProfile for hostsWithRefs fails",
|
||||||
|
customExpectations: func(t *testing.T, ds *mock.Store, cfg *config.FleetConfig, appleStore *mock.MDMAppleStore, commander *apple_mdm.MDMAppleCommander) {
|
||||||
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
||||||
|
return []fleet.SCEPIdentityAssociation{{HostUUID: "hostUUID1", EnrollReference: "ref1"}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appleStore.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) {
|
||||||
|
return map[string]error{}, errors.New("foo")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx, logger, ds, cfg, appleStorage, commander := setupTest(t)
|
||||||
|
|
||||||
|
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||||
|
appCfg := &fleet.AppConfig{}
|
||||||
|
appCfg.OrgInfo.OrgName = "fl33t"
|
||||||
|
appCfg.ServerSettings.ServerURL = "https://foo.example.com"
|
||||||
|
return appCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.GetHostCertAssociationsToExpireFunc = func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
|
||||||
|
return []fleet.SCEPIdentityAssociation{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.SetCommandForPendingSCEPRenewalFunc = func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appleStorage.RetrievePushInfoFunc = func(ctx context.Context, targets []string) (map[string]*mdm.Push, error) {
|
||||||
|
pushes := make(map[string]*mdm.Push, len(targets))
|
||||||
|
for _, uuid := range targets {
|
||||||
|
pushes[uuid] = &mdm.Push{
|
||||||
|
PushMagic: "magic" + uuid,
|
||||||
|
Token: []byte("token" + uuid),
|
||||||
|
Topic: "topic" + uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appleStorage.RetrievePushCertFunc = func(ctx context.Context, topic string) (*tls.Certificate, string, error) {
|
||||||
|
cert, err := tls.LoadX509KeyPair("./testdata/server.pem", "./testdata/server.key")
|
||||||
|
return &cert, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.customExpectations(t, ds, cfg, appleStorage, commander)
|
||||||
|
|
||||||
|
err := RenewSCEPCertificates(ctx, logger, ds, cfg, commander)
|
||||||
|
if tc.expectedError {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -85,6 +85,7 @@ type integrationMDMTestSuite struct {
|
|||||||
onDEPScheduleDone func() // function called when depSchedule.Trigger() job completed
|
onDEPScheduleDone func() // function called when depSchedule.Trigger() job completed
|
||||||
mdmStorage *mysql.NanoMDMStorage
|
mdmStorage *mysql.NanoMDMStorage
|
||||||
worker *worker.Worker
|
worker *worker.Worker
|
||||||
|
mdmCommander *apple_mdm.MDMAppleCommander
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *integrationMDMTestSuite) SetupSuite() {
|
func (s *integrationMDMTestSuite) SetupSuite() {
|
||||||
@ -200,6 +201,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||||||
s.depSchedule = depSchedule
|
s.depSchedule = depSchedule
|
||||||
s.profileSchedule = profileSchedule
|
s.profileSchedule = profileSchedule
|
||||||
s.mdmStorage = mdmStorage
|
s.mdmStorage = mdmStorage
|
||||||
|
s.mdmCommander = mdmCommander
|
||||||
|
|
||||||
macosJob := &worker.MacosSetupAssistant{
|
macosJob := &worker.MacosSetupAssistant{
|
||||||
Datastore: s.ds,
|
Datastore: s.ds,
|
||||||
@ -7092,8 +7094,13 @@ func (s *integrationMDMTestSuite) downloadAndVerifyEnrollmentProfile(path string
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(body), headerLen)
|
require.Equal(t, len(body), headerLen)
|
||||||
|
|
||||||
|
return s.verifyEnrollmentProfile(body, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *integrationMDMTestSuite) verifyEnrollmentProfile(rawProfile []byte, enrollmentRef string) *enrollmentProfile {
|
||||||
|
t := s.T()
|
||||||
var profile enrollmentProfile
|
var profile enrollmentProfile
|
||||||
require.NoError(t, plist.Unmarshal(body, &profile))
|
require.NoError(t, plist.Unmarshal(rawProfile, &profile))
|
||||||
|
|
||||||
for _, p := range profile.PayloadContent {
|
for _, p := range profile.PayloadContent {
|
||||||
switch p.PayloadType {
|
switch p.PayloadType {
|
||||||
@ -7101,8 +7108,10 @@ func (s *integrationMDMTestSuite) downloadAndVerifyEnrollmentProfile(path string
|
|||||||
require.Equal(t, s.getConfig().ServerSettings.ServerURL+apple_mdm.SCEPPath, p.PayloadContent.URL)
|
require.Equal(t, s.getConfig().ServerSettings.ServerURL+apple_mdm.SCEPPath, p.PayloadContent.URL)
|
||||||
require.Equal(t, s.fleetCfg.MDM.AppleSCEPChallenge, p.PayloadContent.Challenge)
|
require.Equal(t, s.fleetCfg.MDM.AppleSCEPChallenge, p.PayloadContent.Challenge)
|
||||||
case "com.apple.mdm":
|
case "com.apple.mdm":
|
||||||
// Use Contains as the url may have query params
|
|
||||||
require.Contains(t, p.ServerURL, s.getConfig().ServerSettings.ServerURL+apple_mdm.MDMPath)
|
require.Contains(t, p.ServerURL, s.getConfig().ServerSettings.ServerURL+apple_mdm.MDMPath)
|
||||||
|
if enrollmentRef != "" {
|
||||||
|
require.Contains(t, p.ServerURL, enrollmentRef)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
require.Failf(t, "unrecognized payload type in enrollment profile: %s", p.PayloadType)
|
require.Failf(t, "unrecognized payload type in enrollment profile: %s", p.PayloadType)
|
||||||
}
|
}
|
||||||
@ -11323,3 +11332,112 @@ func (s *integrationMDMTestSuite) TestZCustomConfigurationWebURL() {
|
|||||||
func (s *integrationMDMTestSuite) TestGetManualEnrollmentProfile() {
|
func (s *integrationMDMTestSuite) TestGetManualEnrollmentProfile() {
|
||||||
s.downloadAndVerifyEnrollmentProfile("/api/latest/fleet/mdm/manual_enrollment_profile")
|
s.downloadAndVerifyEnrollmentProfile("/api/latest/fleet/mdm/manual_enrollment_profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *integrationMDMTestSuite) TestSCEPCertExpiration() {
|
||||||
|
t := s.T()
|
||||||
|
ctx := context.Background()
|
||||||
|
// ensure there's a token for automatic enrollments
|
||||||
|
s.mockDEPResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
|
||||||
|
}))
|
||||||
|
s.runDEPSchedule()
|
||||||
|
|
||||||
|
// add a device that's manually enrolled
|
||||||
|
desktopToken := uuid.New().String()
|
||||||
|
manualHost := createOrbitEnrolledHost(t, "darwin", "h1", s.ds)
|
||||||
|
err := s.ds.SetOrUpdateDeviceAuthToken(context.Background(), manualHost.ID, desktopToken)
|
||||||
|
require.NoError(t, err)
|
||||||
|
manualEnrolledDevice := mdmtest.NewTestMDMClientAppleDesktopManual(s.server.URL, desktopToken)
|
||||||
|
manualEnrolledDevice.UUID = manualHost.UUID
|
||||||
|
err = manualEnrolledDevice.Enroll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// add a device that's automatically enrolled
|
||||||
|
automaticHost := createOrbitEnrolledHost(t, "darwin", "h2", s.ds)
|
||||||
|
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
|
||||||
|
automaticEnrolledDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
|
||||||
|
automaticEnrolledDevice.UUID = automaticHost.UUID
|
||||||
|
automaticEnrolledDevice.SerialNumber = automaticHost.HardwareSerial
|
||||||
|
err = automaticEnrolledDevice.Enroll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// add a device that's automatically enrolled with a server ref
|
||||||
|
automaticHostWithRef := createOrbitEnrolledHost(t, "darwin", "h3", s.ds)
|
||||||
|
automaticEnrolledDeviceWithRef := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
|
||||||
|
automaticEnrolledDeviceWithRef.UUID = automaticHostWithRef.UUID
|
||||||
|
automaticEnrolledDeviceWithRef.SerialNumber = automaticHostWithRef.HardwareSerial
|
||||||
|
err = automaticEnrolledDeviceWithRef.Enroll()
|
||||||
|
require.NoError(t, s.ds.SetOrUpdateMDMData(ctx, automaticHostWithRef.ID, false, true, s.server.URL, true, fleet.WellKnownMDMFleet, "foo"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cert, key, err := generateCertWithAPNsTopic()
|
||||||
|
require.NoError(t, err)
|
||||||
|
fleetCfg := config.TestConfig()
|
||||||
|
config.SetTestMDMConfig(s.T(), &fleetCfg, cert, key, testBMToken, "")
|
||||||
|
logger := kitlog.NewJSONLogger(os.Stdout)
|
||||||
|
|
||||||
|
// run without expired certs, no command enqueued
|
||||||
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
||||||
|
require.NoError(t, err)
|
||||||
|
cmd, err := manualEnrolledDevice.Idle()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, cmd)
|
||||||
|
|
||||||
|
cmd, err = automaticEnrolledDevice.Idle()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, cmd)
|
||||||
|
|
||||||
|
cmd, err = automaticEnrolledDeviceWithRef.Idle()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, cmd)
|
||||||
|
|
||||||
|
// expire all the certs we just created
|
||||||
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||||
|
_, err := q.ExecContext(ctx, `
|
||||||
|
UPDATE nano_cert_auth_associations
|
||||||
|
SET cert_not_valid_after = DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
|
||||||
|
WHERE id IN (?, ?, ?)
|
||||||
|
`, manualHost.UUID, automaticHost.UUID, automaticHostWithRef.UUID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
// generate a new config here so we can manipulate the certs.
|
||||||
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
checkRenewCertCommand := func(device *mdmtest.TestAppleMDMClient, enrollRef string) {
|
||||||
|
var renewCmd *micromdm.CommandPayload
|
||||||
|
cmd, err := device.Idle()
|
||||||
|
require.NoError(t, err)
|
||||||
|
for cmd != nil {
|
||||||
|
if cmd.Command.RequestType == "InstallProfile" {
|
||||||
|
renewCmd = cmd
|
||||||
|
}
|
||||||
|
cmd, err = device.Acknowledge(cmd.CommandUUID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.NotNil(t, renewCmd)
|
||||||
|
s.verifyEnrollmentProfile(renewCmd.Command.InstallProfile.Payload, enrollRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRenewCertCommand(manualEnrolledDevice, "")
|
||||||
|
checkRenewCertCommand(automaticEnrolledDevice, "")
|
||||||
|
checkRenewCertCommand(automaticEnrolledDeviceWithRef, "foo")
|
||||||
|
|
||||||
|
// another cron run shouldn't enqueue more commands
|
||||||
|
err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cmd, err = manualEnrolledDevice.Idle()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, cmd)
|
||||||
|
|
||||||
|
cmd, err = automaticEnrolledDevice.Idle()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, cmd)
|
||||||
|
|
||||||
|
cmd, err = automaticEnrolledDeviceWithRef.Idle()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, cmd)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user