fleet/ee/server/integrationtest/condaccess/scep_rate_limit_test.go
Victor Lyuboslavsky 7c9c5b9a2e
Okta SCEP endpoint (#34721)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #34542

- Added SCEP endpoint for issuing certs for conditional access for Okta.
Functionally similar to host identity and Apple MDM SCEP endpoints.
- Changes file will be added later (this is a sub-task of the feature).
- A standard SCEP payload can be used to get a cert to an Apple device:

```
<!-- SCEP Configuration -->
<dict>
	<key>PayloadContent</key>
	<dict>
		<key>URL</key>
		<string>https://myfleet.example.com/api/fleet/conditional_access/scep</string>
		<key>Challenge</key>
		<string>ENROLLMENT_SECRET</string>
		<key>Keysize</key>
		<integer>2048</integer>
		<key>Key Type</key>
		<string>RSA</string>
		<key>Key Usage</key>
		<integer>5</integer>
              <key>ExtendedKeyUsage</key>
              <array>
                  <string>1.3.6.1.5.5.7.3.2</string>
              </array>
		<key>Subject</key>
		<array>
			<array>
				<array>
					<string>CN</string>
					<string>Fleet conditional access for Okta</string>
				</array>
			</array>
		</array>
		<key>SubjectAltName</key>
		<dict>
			<key>uniformResourceIdentifier</key>
			<array>
				<string>urn:device:apple:uuid:%HardwareUUID%</string>
			</array>
		</dict>
		<key>Retries</key>
		<integer>3</integer>
		<key>RetryDelay</key>
		<integer>10</integer>
              <!-- ACL for browser access -->
              <key>AllowAllAppsAccess</key>
              <true/>
              <!-- Set true for Safari access. Set false if Safari support not needed. -->
              <key>KeyIsExtractable</key>
              <false/>
	</dict>
	<key>PayloadDescription</key>
	<string>Configures SCEP for Fleet conditional access for Okta certificate</string>
	<key>PayloadDisplayName</key>
	<string>Fleet conditional access SCEP</string>
	<key>PayloadIdentifier</key>
	<string>com.fleetdm.conditional-access-scep</string>
	<key>PayloadType</key>
	<string>com.apple.security.scep</string>
	<key>PayloadUUID</key>
	<string>B2C3D4E5-F6A7-4B6C-9D8E-0F1A2B3C4D5E</string>
	<key>PayloadVersion</key>
	<integer>1</integer>
</dict>
```

# Checklist for submitter

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

## Database migrations

- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## New Features
* Adds Conditional Access SCEP certificate enrollment support, enabling
hosts to obtain device identity certificates through secure certificate
enrollment protocol endpoints.
* Implements rate limiting for certificate enrollment requests to
prevent abuse.

## Tests
* Adds comprehensive integration tests for Conditional Access SCEP
functionality, including certificate operations, rate limiting
validation, and edge cases.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-06 17:07:17 -06:00

71 lines
2.7 KiB
Go

package condaccess
import (
"net/http"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSCEPRateLimit(t *testing.T) {
// Set up suite with rate limiting configuration
cooldown := 5 * time.Minute
s := SetUpSuiteWithConfig(t, "integrationtest.ConditionalAccessSCEPRateLimit", func(cfg *config.FleetConfig) {
cfg.Osquery.EnrollCooldown = cooldown
})
defer mysql.TruncateTables(t, s.BaseSuite.DS, []string{
"conditional_access_scep_serials", "conditional_access_scep_certificates",
}...)
t.Run("RateLimitSameHost", func(t *testing.T) {
ctx := t.Context()
// Create enrollment secret
err := s.DS.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: testEnrollmentSecret}})
require.NoError(t, err)
// Create a test host
host, err := s.DS.NewHost(ctx, &fleet.Host{
OsqueryHostID: ptr.String("test-host-rate-limit"),
NodeKey: ptr.String("test-node-key-rate-limit"),
UUID: "test-uuid-rate-limit",
Hostname: "test-hostname-rate-limit",
Platform: "darwin",
DetailUpdatedAt: time.Now(),
})
require.NoError(t, err)
// First certificate request - should succeed
cert1 := requestSCEPCertificate(t, s, host.UUID, testEnrollmentSecret)
require.NotNil(t, cert1, "First certificate request should succeed")
assert.Equal(t, "urn:device:apple:uuid:"+host.UUID, cert1.URIs[0].String())
// Second certificate request immediately after - should fail due to rate limit
httpResp, pkiMsgResp, cert2 := requestSCEPCertificateWithChallenge(t, s, host.UUID, testEnrollmentSecret)
require.Equal(t, http.StatusTooManyRequests, httpResp.StatusCode, "Should return HTTP 429 for rate limit")
require.Nil(t, pkiMsgResp, "PKI message not parsed for rate limit errors")
require.Nil(t, cert2, "Second certificate request should fail due to rate limit")
// Different host should be able to get certificate
differentHost, err := s.DS.NewHost(ctx, &fleet.Host{
OsqueryHostID: ptr.String("test-host-different"),
NodeKey: ptr.String("test-node-key-different"),
UUID: "test-uuid-different",
Hostname: "test-hostname-different",
Platform: "darwin",
DetailUpdatedAt: time.Now(),
})
require.NoError(t, err)
certDifferent := requestSCEPCertificate(t, s, differentHost.UUID, testEnrollmentSecret)
require.NotNil(t, certDifferent, "Different host should be able to get certificate")
assert.Equal(t, "urn:device:apple:uuid:"+differentHost.UUID, certDifferent.URIs[0].String())
})
}