add CLI for the new MDM cert flow (#19240)
for #19022 # 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] Added/updated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
@@ -21,6 +21,9 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var ErrGeneric = errors.New(`Something's gone wrong. Please try again. If this keeps happening please file an issue:
|
||||
https://github.com/fleetdm/fleet/issues/new/choose`)
|
||||
|
||||
func unauthenticatedClientFromCLI(c *cli.Context) (*service.Client, error) {
|
||||
cc, err := clientConfigFromCLI(c)
|
||||
if err != nil {
|
||||
|
||||
+45
-83
@@ -4,22 +4,18 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
apnsKeyPath = "fleet-mdm-apple-apns.key"
|
||||
scepCACertPath = "fleet-mdm-apple-scep.crt"
|
||||
scepCAKeyPath = "fleet-mdm-apple-scep.key"
|
||||
apnsCSRPath = "fleet-mdm-csr.csr"
|
||||
bmPublicKeyCertPath = "fleet-apple-mdm-bm-public-key.crt"
|
||||
bmPrivateKeyPath = "fleet-apple-mdm-bm-private.key"
|
||||
)
|
||||
|
||||
func generateCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "generate",
|
||||
Usage: "Generate certificates and keys required for MDM",
|
||||
Usage: "Generate certificates and keys required for MDM.",
|
||||
Flags: []cli.Flag{
|
||||
configFlag(),
|
||||
contextFlag(),
|
||||
@@ -36,91 +32,54 @@ func generateMDMAppleCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "mdm-apple",
|
||||
Aliases: []string{"mdm_apple"},
|
||||
Usage: "Generates certificate signing request (CSR) and key for Apple Push Notification Service (APNs) and certificate and key for Simple Certificate Enrollment Protocol (SCEP) to turn on MDM features.",
|
||||
Usage: "Generates certificate signing request (CSR) to turn on MDM features.",
|
||||
Flags: []cli.Flag{
|
||||
contextFlag(),
|
||||
debugFlag(),
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Usage: "The email address to send the signed APNS csr to.",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "org",
|
||||
Usage: "The organization requesting the signed APNS csr.",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "apns-key",
|
||||
Usage: "The output path for the APNs private key.",
|
||||
Value: apnsKeyPath,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scep-cert",
|
||||
Usage: "The output path for the SCEP CA certificate.",
|
||||
Value: scepCACertPath,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scep-key",
|
||||
Usage: "The output path for the SCEP CA private key.",
|
||||
Value: scepCAKeyPath,
|
||||
Name: "csr",
|
||||
Usage: "The output path for the APNs CSR.",
|
||||
Value: apnsCSRPath,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
email := c.String("email")
|
||||
org := c.String("org")
|
||||
apnsKeyPath := c.String("apns-key")
|
||||
scepCACertPath := c.String("scep-cert")
|
||||
scepCAKeyPath := c.String("scep-key")
|
||||
csrPath := c.String("csr")
|
||||
|
||||
// get the fleet API client first, so that any login requirement are met
|
||||
// before printing the CSR output message.
|
||||
client, err := clientFromCLI(c)
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s", err)
|
||||
return ErrGeneric
|
||||
}
|
||||
|
||||
fmt.Fprintf(
|
||||
c.App.Writer,
|
||||
`Sending certificate signing request (CSR) for Apple Push Notification service (APNs) to %s...
|
||||
Generating APNs key, Simple Certificate Enrollment Protocol (SCEP) certificate, and SCEP key...
|
||||
|
||||
`,
|
||||
email,
|
||||
)
|
||||
|
||||
csr, err := client.RequestAppleCSR(email, org)
|
||||
csr, err := client.RequestAppleCSR()
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Fprintf(c.App.ErrWriter, "requesting APNs CSR: %s", err)
|
||||
return ErrGeneric
|
||||
}
|
||||
|
||||
if err := os.WriteFile(apnsKeyPath, csr.APNsKey, defaultFileMode); err != nil {
|
||||
return fmt.Errorf("failed to write APNs private key: %w", err)
|
||||
if err := os.WriteFile(csrPath, csr, defaultFileMode); err != nil {
|
||||
fmt.Fprintf(c.App.ErrWriter, "write CSR: %s", err)
|
||||
return ErrGeneric
|
||||
}
|
||||
if err := os.WriteFile(scepCACertPath, csr.SCEPCert, defaultFileMode); err != nil {
|
||||
return fmt.Errorf("failed to write SCEP CA certificate: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(scepCAKeyPath, csr.SCEPKey, defaultFileMode); err != nil {
|
||||
return fmt.Errorf("failed to write SCEP CA private key: %w", err)
|
||||
|
||||
appCfg, err := client.GetAppConfig()
|
||||
if err != nil {
|
||||
fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s", err)
|
||||
return ErrGeneric
|
||||
}
|
||||
|
||||
fmt.Fprintf(
|
||||
c.App.Writer,
|
||||
`Success!
|
||||
|
||||
Generated your APNs key at %s
|
||||
Generated your certificate signing request (CSR) at %s
|
||||
|
||||
Generated your SCEP certificate at %s
|
||||
|
||||
Generated your SCEP key at %s
|
||||
|
||||
Go to your email to download a CSR from Fleet. Then, visit https://identity.apple.com/pushcert to upload the CSR. You should receive an APNs certificate in return from Apple.
|
||||
|
||||
Next, use the generated certificates to deploy Fleet with `+"`mdm`"+` configuration: https://fleetdm.com/docs/deploying/configuration#mobile-device-management-mdm
|
||||
Go to %s/settings/integrations/mdm/apple and follow the steps.
|
||||
`,
|
||||
apnsKeyPath,
|
||||
scepCACertPath,
|
||||
scepCAKeyPath,
|
||||
csrPath,
|
||||
appCfg.ServerSettings.ServerURL,
|
||||
)
|
||||
|
||||
return nil
|
||||
@@ -132,7 +91,7 @@ func generateMDMAppleBMCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "mdm-apple-bm",
|
||||
Aliases: []string{"mdm_apple_bm"},
|
||||
Usage: "Generate Apple Business Manager public and private keys to enable automatic enrollment for macOS hosts.",
|
||||
Usage: "Generate Apple Business Manager public key to enable automatic enrollment for macOS hosts.",
|
||||
Flags: []cli.Flag{
|
||||
contextFlag(),
|
||||
debugFlag(),
|
||||
@@ -141,27 +100,33 @@ func generateMDMAppleBMCommand() *cli.Command {
|
||||
Usage: "The output path for the Apple Business Manager public key certificate.",
|
||||
Value: bmPublicKeyCertPath,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "private-key",
|
||||
Usage: "The output path for the Apple Business Manager private key.",
|
||||
Value: bmPrivateKeyPath,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
publicKeyPath := c.String("public-key")
|
||||
privateKeyPath := c.String("private-key")
|
||||
|
||||
publicKeyPEM, privateKeyPEM, err := apple_mdm.NewDEPKeyPairPEM()
|
||||
// get the fleet API client first, so that any login requirement are met
|
||||
// before printing the CSR output message.
|
||||
client, err := clientFromCLI(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate key pair: %w", err)
|
||||
fmt.Fprintf(c.App.ErrWriter, "client from CLI: %s", err)
|
||||
return ErrGeneric
|
||||
}
|
||||
|
||||
if err := os.WriteFile(publicKeyPath, publicKeyPEM, defaultFileMode); err != nil {
|
||||
return fmt.Errorf("write public key: %w", err)
|
||||
publicKey, err := client.RequestAppleABM()
|
||||
if err != nil {
|
||||
fmt.Fprintf(c.App.ErrWriter, "requesting ABM public key: %s", err)
|
||||
return ErrGeneric
|
||||
}
|
||||
|
||||
if err := os.WriteFile(privateKeyPath, privateKeyPEM, defaultFileMode); err != nil {
|
||||
return fmt.Errorf("write private key: %w", err)
|
||||
if err := os.WriteFile(publicKeyPath, publicKey, defaultFileMode); err != nil {
|
||||
fmt.Fprintf(c.App.ErrWriter, "write public key: %s", err)
|
||||
return ErrGeneric
|
||||
}
|
||||
|
||||
appCfg, err := client.GetAppConfig()
|
||||
if err != nil {
|
||||
fmt.Fprintf(c.App.ErrWriter, "fetching app config: %s", err)
|
||||
return ErrGeneric
|
||||
}
|
||||
|
||||
fmt.Fprintf(
|
||||
@@ -170,14 +135,11 @@ func generateMDMAppleBMCommand() *cli.Command {
|
||||
|
||||
Generated your public key at %s
|
||||
|
||||
Generated your private key at %s
|
||||
Go to %s/settings/integrations/automatic-enrollment/apple and follow the steps.
|
||||
|
||||
Visit https://business.apple.com/ and create a new MDM server with the public key. Then, download the new MDM server's token.
|
||||
|
||||
Next, deploy Fleet with with `+"`mdm`"+` configuration: https://fleetdm.com/docs/deploying/configuration#mobile-device-management-mdm
|
||||
`,
|
||||
publicKeyPath,
|
||||
privateKeyPath,
|
||||
appCfg.ServerSettings.ServerURL,
|
||||
)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -14,37 +14,33 @@ import (
|
||||
)
|
||||
|
||||
func TestGenerateMDMAppleBM(t *testing.T) {
|
||||
// TODO(roberto): update when the new endpoint to get a CSR is ready
|
||||
t.Skip()
|
||||
outdir, err := os.MkdirTemp("", t.Name())
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(outdir)
|
||||
publicKeyPath := filepath.Join(outdir, "public-key.crt")
|
||||
privateKeyPath := filepath.Join(outdir, "private-key.key")
|
||||
|
||||
out := runAppForTest(t, []string{
|
||||
"generate", "mdm-apple-bm",
|
||||
"--public-key", publicKeyPath,
|
||||
"--private-key", privateKeyPath,
|
||||
})
|
||||
|
||||
require.Contains(t, out, fmt.Sprintf("Generated your public key at %s", outdir))
|
||||
require.Contains(t, out, fmt.Sprintf("Generated your private key at %s", outdir))
|
||||
|
||||
// validate that the keypair is valid
|
||||
cert, err := tls.LoadX509KeyPair(publicKeyPath, privateKeyPath)
|
||||
// validate that the certificate is valid
|
||||
certPEMBlock, err := os.ReadFile(publicKeyPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
parsed, err := x509.ParseCertificate(cert.Certificate[0])
|
||||
parsed, err := x509.ParseCertificate(certPEMBlock)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "FleetDM", parsed.Issuer.CommonName)
|
||||
}
|
||||
|
||||
func TestGenerateMDMApple(t *testing.T) {
|
||||
t.Run("missing input", func(t *testing.T) {
|
||||
runAppCheckErr(t, []string{"generate", "mdm-apple"}, `Required flags "email, org" not set`)
|
||||
runAppCheckErr(t, []string{"generate", "mdm-apple", "--email", "user@example.com"}, `Required flag "org" not set`)
|
||||
runAppCheckErr(t, []string{"generate", "mdm-apple", "--org", "Acme"}, `Required flag "email" not set`)
|
||||
})
|
||||
|
||||
t.Run("CSR API call fails", func(t *testing.T) {
|
||||
// TODO(roberto): update when the new endpoint to get a CSR is ready
|
||||
t.Skip()
|
||||
_, _ = runServerWithMockedDS(t)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// fail this call
|
||||
@@ -57,14 +53,14 @@ func TestGenerateMDMApple(t *testing.T) {
|
||||
t,
|
||||
[]string{
|
||||
"generate", "mdm-apple",
|
||||
"--email", "user@example.com",
|
||||
"--org", "Acme",
|
||||
},
|
||||
`POST /api/latest/fleet/mdm/apple/request_csr received status 422 Validation Failed: this email address is not valid: bad request`,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("successful run", func(t *testing.T) {
|
||||
// TODO(roberto): update when the new endpoint to get a CSR is ready
|
||||
t.Skip()
|
||||
_, _ = runServerWithMockedDS(t)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -76,29 +72,24 @@ func TestGenerateMDMApple(t *testing.T) {
|
||||
outdir, err := os.MkdirTemp("", "TestGenerateMDMApple")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(outdir)
|
||||
apnsKeyPath := filepath.Join(outdir, "apns.key")
|
||||
scepCertPath := filepath.Join(outdir, "scep.crt")
|
||||
scepKeyPath := filepath.Join(outdir, "scep.key")
|
||||
csrPath := filepath.Join(outdir, "csr.csr")
|
||||
out := runAppForTest(t, []string{
|
||||
"generate", "mdm-apple",
|
||||
"--email", "user@example.com",
|
||||
"--org", "Acme",
|
||||
"--apns-key", apnsKeyPath,
|
||||
"--scep-cert", scepCertPath,
|
||||
"--scep-key", scepKeyPath,
|
||||
"--csr", csrPath,
|
||||
"--debug",
|
||||
"--context", "default",
|
||||
})
|
||||
|
||||
require.Contains(t, out, fmt.Sprintf("Generated your APNs key at %s", apnsKeyPath))
|
||||
require.Contains(t, out, fmt.Sprintf("Generated your SCEP certificate at %s", scepCertPath))
|
||||
require.Contains(t, out, fmt.Sprintf("Generated your SCEP key at %s", scepKeyPath))
|
||||
require.Contains(t, out, fmt.Sprintf("Generated your SCEP key at %s", csrPath))
|
||||
|
||||
// validate that the keypair is valid
|
||||
scepCrt, err := tls.LoadX509KeyPair(scepCertPath, scepKeyPath)
|
||||
// validate that the CSR is valid
|
||||
csrPEM, err := os.ReadFile(csrPath)
|
||||
require.NoError(t, err)
|
||||
parsed, err := x509.ParseCertificate(scepCrt.Certificate[0])
|
||||
|
||||
block, _ := pem.Decode(csrPEM)
|
||||
require.NotNil(t, block)
|
||||
require.Equal(t, "CERTIFICATE REQUEST", block.Type)
|
||||
_, err = x509.ParseCertificateRequest(block.Bytes)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "FleetDM", parsed.Issuer.CommonName)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ type bodyHandler interface {
|
||||
|
||||
type FileResponse struct {
|
||||
DestPath string
|
||||
DestFile string
|
||||
destFilePath string
|
||||
}
|
||||
|
||||
|
||||
@@ -41,16 +41,23 @@ func (c *Client) GetAppleBM() (*fleet.AppleBM, error) {
|
||||
}
|
||||
|
||||
// RequestAppleCSR requests a signed CSR from the Fleet server and returns the
|
||||
// SCEP certificate and key along with the APNs key used for the CSR.
|
||||
func (c *Client) RequestAppleCSR(email, org string) (*fleet.AppleCSR, error) {
|
||||
verb, path := "POST", "/api/latest/fleet/mdm/apple/request_csr"
|
||||
request := requestMDMAppleCSRRequest{
|
||||
EmailAddress: email,
|
||||
Organization: org,
|
||||
}
|
||||
var responseBody requestMDMAppleCSRResponse
|
||||
err := c.authenticatedRequest(request, verb, path, &responseBody)
|
||||
return responseBody.AppleCSR, err
|
||||
// CSR bytes
|
||||
func (c *Client) RequestAppleCSR() ([]byte, error) {
|
||||
verb, path := "GET", "/api/v1/fleet/mdm/apple/request_csr"
|
||||
// TODO(roberto): adjust request/response type when the endpoint is ready
|
||||
var request, resp map[string][]byte
|
||||
err := c.authenticatedRequest(request, verb, path, &resp)
|
||||
return resp["csr"], err
|
||||
}
|
||||
|
||||
// RequestAppleABM requests a signed CSR from the Fleet server and returns the
|
||||
// public key bytes
|
||||
func (c *Client) RequestAppleABM() ([]byte, error) {
|
||||
verb, path := "GET", "/api/v1/fleet/mdm/apple/abm_public_key?alt=media"
|
||||
// TODO(roberto): adjust this request type when the endpoint is ready
|
||||
var request, resp map[string][]byte
|
||||
err := c.authenticatedRequest(request, verb, path, &resp)
|
||||
return resp["public_key"], err
|
||||
}
|
||||
|
||||
func (c *Client) GetBootstrapPackageMetadata(teamID uint, forUpdate bool) (*fleet.MDMAppleBootstrapPackage, error) {
|
||||
|
||||
Reference in New Issue
Block a user