fleet/android
Dante Catalfamo b2391c80b7
Remove certificates from device when missing from managed config (#37198)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #36690

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [ ] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [ ] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [ ] Confirmed that the fix is not expected to adversely impact load
test results
- [ ] Alerted the release DRI if additional load testing is needed

## Database migrations

- [ ] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ ] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).

## New Fleet configuration settings

- [ ] Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for
GitOps-enabled settings:

- [ ] Verified that the setting is exported via `fleetctl
generate-gitops`
- [ ] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [ ] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [ ] Verified that any relevant UI is disabled when GitOps mode is
enabled

## fleetd/orbit/Fleet Desktop

- [ ] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
- [ ] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
- [ ] Verified that fleetd runs on macOS, Linux and Windows
- [ ] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))


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

## Summary by CodeRabbit

* **New Features**
* Automatic cleanup of certificates that have been removed from your
system, now executed automatically before enrolling new certificates
* Enhanced certificate operation tracking with improved status reporting
for installation and removal operations, providing better visibility
into certificate lifecycle events

* **Bug Fixes**
* Fixed back navigation behavior on the Debug screen, improving
navigation flow for users

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Victor Lyuboslavsky <2685025+getvictor@users.noreply.github.com>
2025-12-22 16:28:50 -06:00
..
app Remove certificates from device when missing from managed config (#37198) 2025-12-22 16:28:50 -06:00
gradle Fleet Android UI (#37345) 2025-12-16 16:23:53 -06:00
.editorconfig Moved lint rules to .editorconfig for Android Studio (#36302) 2025-11-25 15:37:21 -06:00
.gitignore Added README for Android (#36140) 2025-11-24 15:51:52 -06:00
build.gradle.kts Android app scaffold (#35901) 2025-11-20 13:54:55 -06:00
gradle.properties Android app scaffold (#35901) 2025-11-20 13:54:55 -06:00
gradlew Android app scaffold (#35901) 2025-11-20 13:54:55 -06:00
gradlew.bat Android app scaffold (#35901) 2025-11-20 13:54:55 -06:00
README.md Updated Android README.md and created RELEASE.md (#37122) 2025-12-12 09:07:02 -06:00
RELEASE.md Updated Android README.md and created RELEASE.md (#37122) 2025-12-12 09:07:02 -06:00
settings.gradle.kts Android app scaffold (#35901) 2025-11-20 13:54:55 -06:00

Fleet Android agent

Requirements

  • JDK 17 or later - Set JAVA_HOME environment variable
  • Android SDK - Gradle finds it via:
    • local.properties file with sdk.dir (auto-created by Android Studio) Recommended
    • OR ANDROID_HOME / ANDROID_SDK_ROOT environment variables
    • Install via Android Studio (easiest)
    • Or install command-line tools
    • Requires SDK Platform API 33+ and Build Tools 34.0.0+

Building the project

Debug build

./gradlew assembleDebug

Output: app/build/outputs/apk/debug/app-debug.apk

Release build

./gradlew assembleRelease

Output: app/build/outputs/apk/release/app-release.apk

Note: By default (without signing configuration), this creates an unsigned APK not suitable for distribution.

Signing release builds

Signing configuration is already set up in build.gradle.kts. You just need to provide the keystore and credentials.

One-time setup per developer/machine:

  1. Create a keystore:
keytool -genkeypair \
  -alias fleet-android \
  -keyalg RSA \
  -keysize 4096 \
  -validity 10000 \
  -keystore keystore.jks \
  -storepass YOUR_PASSWORD \
  -dname "CN=Your Name, O=Your Org, L=City, ST=State, C=US"
  1. Create keystore.properties file in the android/ directory:
storeFile=/path/to/keystore.jks
storePassword=YOUR_PASSWORD
keyAlias=fleet-android
keyPassword=YOUR_PASSWORD
  1. Build signed release:
# APK (for direct distribution)
./gradlew assembleRelease

# AAB (for Google Play Store)
./gradlew bundleRelease

Output:

  • APK: app/build/outputs/apk/release/app-release.apk
  • AAB: app/build/outputs/bundle/release/app-release.aab

Verify signing:

# APK - use apksigner (in SDK build-tools)
# Find your SDK and build-tools version:
grep sdk.dir local.properties
ls "$(grep sdk.dir local.properties | cut -d= -f2)/build-tools/"
# Then verify:
<sdk-path>/build-tools/<version>/apksigner verify --verbose app/build/outputs/apk/release/app-release.apk

# AAB - use jarsigner (included with JDK)
jarsigner -verify app/build/outputs/bundle/release/app-release.aab

Getting the SHA256 fingerprint

The SHA256 fingerprint is required for MDM deployment. You can get it from your keystore.

keytool -list -v -keystore keystore.jks -alias fleet-android
# Grab SHA256 (remove colons and convert to base64)
echo <SHA256> | tr -d ':' | xxd -r -p | base64

Copy the fingerprint for use in FLEET_DEV_ANDROID_AGENT_SIGNING_SHA256

Deploying via Android MDM (development)

This feature is behind the feature flag FLEET_DEV_ANDROID_AGENT_PACKAGE. Requires FLEET_DEV_ANDROID_GOOGLE_SERVICE_CREDENTIALS to be set in your workarea to get the Google Play URL.

  1. Set these env vars on your Fleet server:
export FLEET_DEV_ANDROID_AGENT_PACKAGE=com.fleetdm.agent.private.<yourname>
export FLEET_DEV_ANDROID_AGENT_SIGNING_SHA256=<SHA256 fingerprint>
  1. Change the applicationId in app/build.gradle.kts:
defaultConfig {
    applicationId = "com.fleetdm.agent.private.<yourname>"
    // ...
}
  1. Build a signed release (AAB) using the instructions above.

  2. Get the Google Play URL:

# Run from top-level directory of the working tree
go run tools/android/android.go --command enterprises.webTokens.create --enterprise_id '<your-enterprise-id>'
  1. Upload your signed app in the Private apps tab using the URL from the previous step.

  2. Wait ~10 minutes for Google Play to process the upload.

  3. Enroll your Android device.

The agent should start installing shortly. Check Google Play in your Work profile. If it shows as pending, try restarting the device.

How the app starts

The Fleet Android agent is designed to run automatically without user interaction. The app starts in three scenarios:

1. On installation (COMPANION_APP role)

When the app is installed via MDM, Android Device Policy assigns it the COMPANION_APP role. This triggers RoleNotificationReceiverService, which starts the app process and runs AgentApplication.onCreate().

2. On device boot

When the device boots, BootReceiver receives the ACTION_BOOT_COMPLETED broadcast and starts the app process, triggering AgentApplication.onCreate().

3. Periodically every 15 minutes

AgentApplication.onCreate() schedules a ConfigCheckWorker to run every 15 minutes using WorkManager. This ensures the app wakes up periodically even if the process is killed.

Note: WorkManager ensures reliable background execution. The work persists across device reboots and process death.

Why not ACTION_APPLICATION_RESTRICTIONS_CHANGED?

We don't use ACTION_APPLICATION_RESTRICTIONS_CHANGED to detect MDM config changes because:

  1. This broadcast can only be registered dynamically (not in the manifest)
  2. On Android 14+, context-registered broadcasts are queued when the app is in cached state

This means the broadcast won't wake the app immediately when configs change if the app is in the background. WorkManager polling every 15 minutes is the reliable solution for detecting config changes.

Full build with tests

./gradlew build

This runs:

  • Compilation (debug + release)
  • Unit tests
  • Android Lint
  • Spotless formatting checks (automatic)

Running tests

Unit tests (JVM)

./gradlew test

Integration tests (with real SCEP server)

Integration tests are skipped by default. To run them:

./gradlew test -PrunIntegrationTests=true \
  -Pscep.url=https://your-scep-server.com/scep \
  -Pscep.challenge=your-challenge-password

Setting Up a Test SCEP Server

Integration tests require a real SCEP server. Options:

  1. Production-grade SCEP servers:

    • Microsoft NDES (Network Device Enrollment Service)
    • OpenXPKI
    • Ejbca
  2. Lightweight test servers:

    • micromdm/scep (Docker)
    • jscep test server

Docker SCEP Server (Easiest)

docker run -p 8080:8080 \
  -e SCEP_CHALLENGE=test-challenge-123 \
  micromdm/scep:latest

Running Integration Tests

./gradlew test -PrunIntegrationTests=true \
  -Pscep.url=http://localhost:8080/scep \
  -Pscep.challenge=test-challenge-123

Instrumented tests (requires emulator/device)

./gradlew connectedDebugAndroidTest

Code quality

Formatting with Spotless (ktlint)

Check formatting:

./gradlew spotlessCheck

Auto-fix formatting issues:

./gradlew spotlessApply

Note: Spotless checks run automatically during ./gradlew build. Run spotlessApply to fix issues before committing.

Static analysis with Detekt

Run manually:

./gradlew detekt

Note: Detekt does NOT run automatically in local builds (only in CI). Run manually when needed.

Dependencies

See gradle/libs.versions.toml for complete list.

Development workflow

  1. Before committing: Run ./gradlew spotlessApply to fix formatting
  2. Local verification: Run ./gradlew build to ensure everything passes
  3. Optional: Run ./gradlew detekt for static analysis
  4. Push: CI will run all checks automatically

Troubleshooting

Clean build:

./gradlew clean build

Delete device from Android MDM:

  • Delete Work profile on Android device
  • Using tools/android/android.go, delete the device and delete the associated policy (as of 2025/11/21, Fleet server does not do this)