mirror of
https://github.com/siderolabs/talos.git
synced 2025-10-26 14:01:39 +01:00
feat(talosctl): append microsoft secure boot certs
This patch adds a flag to `secureboot.database.Generate` to append the Microsoft UEFI secure boot DB and KEK certificates to the appropriate ESLs, in addition to complimentary command line flags. This patch also includes a copy of said Microsoft certificates. The certificates are downloaded from an official Microsoft repo. Signed-off-by: Jean-Francois Roy <jf@devklog.net> Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
This commit is contained in:
parent
fd6ddd11ef
commit
fd54dc191d
13
Dockerfile
13
Dockerfile
@ -309,6 +309,16 @@ FROM scratch AS ipxe-generate
|
|||||||
COPY --from=pkg-ipxe-amd64 /usr/libexec/snp.efi /amd64/snp.efi
|
COPY --from=pkg-ipxe-amd64 /usr/libexec/snp.efi /amd64/snp.efi
|
||||||
COPY --from=pkg-ipxe-arm64 /usr/libexec/snp.efi /arm64/snp.efi
|
COPY --from=pkg-ipxe-arm64 /usr/libexec/snp.efi /arm64/snp.efi
|
||||||
|
|
||||||
|
FROM scratch AS microsoft-secureboot-database
|
||||||
|
ADD https://github.com/microsoft/secureboot_objects.git /
|
||||||
|
|
||||||
|
FROM scratch AS microsoft-key-keys
|
||||||
|
COPY --from=microsoft-secureboot-database /PreSignedObjects/KEK/Certificates/*.der /kek/
|
||||||
|
|
||||||
|
FROM scratch AS microsoft-db-keys
|
||||||
|
COPY --from=microsoft-secureboot-database /PreSignedObjects/DB/Certificates/MicCor*.der /db/
|
||||||
|
COPY --from=microsoft-secureboot-database /PreSignedObjects/DB/Certificates/microsoft*.der /db/
|
||||||
|
|
||||||
FROM --platform=${BUILDPLATFORM} scratch AS generate
|
FROM --platform=${BUILDPLATFORM} scratch AS generate
|
||||||
COPY --from=proto-format-build /src/api /api/
|
COPY --from=proto-format-build /src/api /api/
|
||||||
COPY --from=generate-build /api/common/*.pb.go /pkg/machinery/api/common/
|
COPY --from=generate-build /api/common/*.pb.go /pkg/machinery/api/common/
|
||||||
@ -333,6 +343,8 @@ COPY --from=go-generate /src/pkg/machinery/extensions/ /pkg/machinery/extensions
|
|||||||
COPY --from=ipxe-generate / /pkg/provision/providers/vm/internal/ipxe/data/ipxe/
|
COPY --from=ipxe-generate / /pkg/provision/providers/vm/internal/ipxe/data/ipxe/
|
||||||
COPY --from=embed-abbrev / /
|
COPY --from=embed-abbrev / /
|
||||||
COPY --from=pkg-ca-certificates /etc/ssl/certs/ca-certificates /internal/app/machined/pkg/controllers/secrets/data/
|
COPY --from=pkg-ca-certificates /etc/ssl/certs/ca-certificates /internal/app/machined/pkg/controllers/secrets/data/
|
||||||
|
COPY --from=microsoft-key-keys / /internal/pkg/secureboot/database/certs/
|
||||||
|
COPY --from=microsoft-db-keys / /internal/pkg/secureboot/database/certs/
|
||||||
|
|
||||||
# The base target provides a container that can be used to build all Talos
|
# The base target provides a container that can be used to build all Talos
|
||||||
# assets.
|
# assets.
|
||||||
@ -345,6 +357,7 @@ COPY --from=generate /pkg/flannel/ ./pkg/flannel/
|
|||||||
COPY --from=generate /pkg/imager/ ./pkg/imager/
|
COPY --from=generate /pkg/imager/ ./pkg/imager/
|
||||||
COPY --from=generate /pkg/machinery/ ./pkg/machinery/
|
COPY --from=generate /pkg/machinery/ ./pkg/machinery/
|
||||||
COPY --from=generate /internal/app/machined/pkg/controllers/secrets/data/ ./internal/app/machined/pkg/controllers/secrets/data/
|
COPY --from=generate /internal/app/machined/pkg/controllers/secrets/data/ ./internal/app/machined/pkg/controllers/secrets/data/
|
||||||
|
COPY --from=generate /internal/pkg/secureboot/database/certs/ ./internal/pkg/secureboot/database/certs/
|
||||||
COPY --from=embed / ./
|
COPY --from=embed / ./
|
||||||
RUN --mount=type=cache,target=/.cache go list all >/dev/null
|
RUN --mount=type=cache,target=/.cache go list all >/dev/null
|
||||||
WORKDIR /src/pkg/machinery
|
WORKDIR /src/pkg/machinery
|
||||||
|
|||||||
@ -45,6 +45,8 @@ var cmdFlags struct {
|
|||||||
OverlayName string
|
OverlayName string
|
||||||
OverlayImage string
|
OverlayImage string
|
||||||
OverlayOptions []string
|
OverlayOptions []string
|
||||||
|
// Only used when generating a secure boot iso without also providing a secure boot database.
|
||||||
|
SecurebootIncludeWellKnownCerts bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// rootCmd represents the base command when called without any subcommands.
|
// rootCmd represents the base command when called without any subcommands.
|
||||||
@ -173,6 +175,13 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
prof.Output.ImageOptions.DiskSize = int64(size)
|
prof.Output.ImageOptions.DiskSize = int64(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmdFlags.SecurebootIncludeWellKnownCerts {
|
||||||
|
if prof.Input.SecureBoot == nil {
|
||||||
|
prof.Input.SecureBoot = &profile.SecureBootAssets{}
|
||||||
|
}
|
||||||
|
prof.Input.SecureBoot.IncludeWellKnownCerts = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(cmdFlags.OutputPath, 0o755); err != nil {
|
if err := os.MkdirAll(cmdFlags.OutputPath, 0o755); err != nil {
|
||||||
@ -229,4 +238,6 @@ func init() {
|
|||||||
rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-name")
|
rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-name")
|
||||||
rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-image")
|
rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-image")
|
||||||
rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-option")
|
rootCmd.MarkFlagsMutuallyExclusive("board", "overlay-option")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(
|
||||||
|
&cmdFlags.SecurebootIncludeWellKnownCerts, "secureboot-include-well-known-certs", false, "Include well-known (Microsoft) UEFI certificates when generating a secure boot database")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,6 +64,7 @@ var genSecurebootPCRCmd = &cobra.Command{
|
|||||||
var genSecurebootDatabaseCmdFlags struct {
|
var genSecurebootDatabaseCmdFlags struct {
|
||||||
enrolledCertificatePath string
|
enrolledCertificatePath string
|
||||||
signingCertificatePath, signingKeyPath string
|
signingCertificatePath, signingKeyPath string
|
||||||
|
includeWellKnownCerts bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// genSecurebootDatabaseCmd represents the `gen secureboot database` command.
|
// genSecurebootDatabaseCmd represents the `gen secureboot database` command.
|
||||||
@ -78,6 +79,7 @@ var genSecurebootDatabaseCmd = &cobra.Command{
|
|||||||
genSecurebootDatabaseCmdFlags.enrolledCertificatePath,
|
genSecurebootDatabaseCmdFlags.enrolledCertificatePath,
|
||||||
genSecurebootDatabaseCmdFlags.signingKeyPath,
|
genSecurebootDatabaseCmdFlags.signingKeyPath,
|
||||||
genSecurebootDatabaseCmdFlags.signingCertificatePath,
|
genSecurebootDatabaseCmdFlags.signingCertificatePath,
|
||||||
|
genSecurebootDatabaseCmdFlags.includeWellKnownCerts,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -140,7 +142,7 @@ func saveAsDER(file string, pem []byte) error {
|
|||||||
// generateSecureBootDatabase generates a UEFI database to enroll the signing certificate.
|
// generateSecureBootDatabase generates a UEFI database to enroll the signing certificate.
|
||||||
//
|
//
|
||||||
// ref: https://blog.hansenpartnership.com/the-meaning-of-all-the-uefi-keys/
|
// ref: https://blog.hansenpartnership.com/the-meaning-of-all-the-uefi-keys/
|
||||||
func generateSecureBootDatabase(path, enrolledCertificatePath, signingKeyPath, signingCertificatePath string) error {
|
func generateSecureBootDatabase(path, enrolledCertificatePath, signingKeyPath, signingCertificatePath string, includeWellKnownCerts bool) error {
|
||||||
in := profile.SigningKeyAndCertificate{
|
in := profile.SigningKeyAndCertificate{
|
||||||
KeyPath: signingKeyPath,
|
KeyPath: signingKeyPath,
|
||||||
CertPath: signingCertificatePath,
|
CertPath: signingCertificatePath,
|
||||||
@ -156,7 +158,7 @@ func generateSecureBootDatabase(path, enrolledCertificatePath, signingKeyPath, s
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := database.Generate(enrolledPEM, signer)
|
db, err := database.Generate(enrolledPEM, signer, database.IncludeWellKnownCertificates(includeWellKnownCerts))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to generate database: %w", err)
|
return fmt.Errorf("failed to generate database: %w", err)
|
||||||
}
|
}
|
||||||
@ -186,6 +188,8 @@ func init() {
|
|||||||
&genSecurebootDatabaseCmdFlags.signingCertificatePath, "signing-certificate", helpers.ArtifactPath(constants.SecureBootSigningCertAsset), "path to the certificate used to sign the database")
|
&genSecurebootDatabaseCmdFlags.signingCertificatePath, "signing-certificate", helpers.ArtifactPath(constants.SecureBootSigningCertAsset), "path to the certificate used to sign the database")
|
||||||
genSecurebootDatabaseCmd.Flags().StringVar(
|
genSecurebootDatabaseCmd.Flags().StringVar(
|
||||||
&genSecurebootDatabaseCmdFlags.signingKeyPath, "signing-key", helpers.ArtifactPath(constants.SecureBootSigningKeyAsset), "path to the key used to sign the database")
|
&genSecurebootDatabaseCmdFlags.signingKeyPath, "signing-key", helpers.ArtifactPath(constants.SecureBootSigningKeyAsset), "path to the key used to sign the database")
|
||||||
|
genSecurebootDatabaseCmd.Flags().BoolVar(
|
||||||
|
&genSecurebootDatabaseCmdFlags.includeWellKnownCerts, "include-well-known-uefi-certs", false, "include well-known UEFI (Microsoft) certificates in the database")
|
||||||
genSecurebootCmd.AddCommand(genSecurebootDatabaseCmd)
|
genSecurebootCmd.AddCommand(genSecurebootDatabaseCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -101,6 +101,12 @@ Talos Linux now supports adding [custom trusted roots](https://www.talos.dev/v1.
|
|||||||
title = "Default Node Labels"
|
title = "Default Node Labels"
|
||||||
description = """\
|
description = """\
|
||||||
Talos Linux on config generation now adds a label `node.kubernetes.io/exclude-from-external-load-balancers` by default for the control plane nodes.
|
Talos Linux on config generation now adds a label `node.kubernetes.io/exclude-from-external-load-balancers` by default for the control plane nodes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[notes.secureboot]
|
||||||
|
title = "Secure Boot"
|
||||||
|
description = """\
|
||||||
|
Talos Linux now can optionally include well-known UEFI (Microsoft) SecureBoot keys into the auto-enrollment UEFI database.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[make_deps]
|
[make_deps]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -7,6 +7,10 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"embed"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/foxboron/go-uefi/efi/signature"
|
"github.com/foxboron/go-uefi/efi/signature"
|
||||||
"github.com/foxboron/go-uefi/efi/util"
|
"github.com/foxboron/go-uefi/efi/util"
|
||||||
@ -23,37 +27,146 @@ type Entry struct {
|
|||||||
Contents []byte
|
Contents []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
microsoftSignatureOwnerGUID = "77fa9abd-0359-4d32-bd60-28f4e78f784b"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Well-known UEFI DB certificates (DER data).
|
||||||
|
//
|
||||||
|
//go:embed certs/db/*.der
|
||||||
|
var wellKnownDB embed.FS
|
||||||
|
|
||||||
|
// Well-known UEFI KEK certificates (PEM data).
|
||||||
|
//
|
||||||
|
//go:embed certs/kek/*.der
|
||||||
|
var wellKnownKEK embed.FS
|
||||||
|
|
||||||
|
func loadWellKnownCertificates(fs embed.FS, path string) ([]*x509.Certificate, error) {
|
||||||
|
certs := []*x509.Certificate{}
|
||||||
|
|
||||||
|
files, err := fs.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
data, err := fs.ReadFile(filepath.Join(path, file.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certs = append(certs, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var wellKnownDBCertificates = sync.OnceValue(func() []*x509.Certificate {
|
||||||
|
certs, err := loadWellKnownCertificates(wellKnownDB, "certs/db")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return certs
|
||||||
|
})
|
||||||
|
|
||||||
|
var wellKnownKEKCertificates = sync.OnceValue(func() []*x509.Certificate {
|
||||||
|
certs, err := loadWellKnownCertificates(wellKnownKEK, "certs/kek")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return certs
|
||||||
|
})
|
||||||
|
|
||||||
|
// Options for Generate.
|
||||||
|
type Options struct {
|
||||||
|
IncludeWellKnownCertificates bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a functional option for Generate.
|
||||||
|
type Option func(*Options)
|
||||||
|
|
||||||
|
// IncludeWellKnownCertificates is an option to include well-known certificates.
|
||||||
|
func IncludeWellKnownCertificates(v bool) Option {
|
||||||
|
return func(o *Options) {
|
||||||
|
o.IncludeWellKnownCertificates = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate generates a UEFI database to enroll the signing certificate.
|
// Generate generates a UEFI database to enroll the signing certificate.
|
||||||
//
|
//
|
||||||
// ref: https://blog.hansenpartnership.com/the-meaning-of-all-the-uefi-keys/
|
// ref: https://blog.hansenpartnership.com/the-meaning-of-all-the-uefi-keys/
|
||||||
func Generate(enrolledCertificate []byte, signer pesign.CertificateSigner) ([]Entry, error) {
|
//
|
||||||
|
//nolint:gocyclo
|
||||||
|
func Generate(enrolledCertificate []byte, signer pesign.CertificateSigner, opts ...Option) ([]Entry, error) {
|
||||||
|
var options Options
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
|
||||||
// derive UUID from enrolled certificate
|
// derive UUID from enrolled certificate
|
||||||
uuid := uuid.NewHash(sha256.New(), uuid.NameSpaceX500, enrolledCertificate, 4)
|
uuid := uuid.NewHash(sha256.New(), uuid.NameSpaceX500, enrolledCertificate, 4)
|
||||||
|
|
||||||
efiGUID := util.StringToGUID(uuid.String())
|
efiGUID := util.StringToGUID(uuid.String())
|
||||||
|
|
||||||
// Create ESL
|
// Create PK ESL
|
||||||
|
pk := signature.NewSignatureDatabase()
|
||||||
|
if err := pk.Append(signature.CERT_X509_GUID, *efiGUID, enrolledCertificate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, signedPK, err := signature.SignEFIVariable(efivar.PK, pk, signer.Signer(), signer.Certificate())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create KEK ESL
|
||||||
|
kek := signature.NewSignatureDatabase()
|
||||||
|
if err := kek.Append(signature.CERT_X509_GUID, *efiGUID, enrolledCertificate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.IncludeWellKnownCertificates {
|
||||||
|
owner := util.StringToGUID(microsoftSignatureOwnerGUID)
|
||||||
|
for _, cert := range wellKnownKEKCertificates() {
|
||||||
|
if err := kek.Append(signature.CERT_X509_GUID, *owner, cert.Raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, signedKEK, err := signature.SignEFIVariable(efivar.KEK, kek, signer.Signer(), signer.Certificate())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create db ESL
|
||||||
db := signature.NewSignatureDatabase()
|
db := signature.NewSignatureDatabase()
|
||||||
if err := db.Append(signature.CERT_X509_GUID, *efiGUID, enrolledCertificate); err != nil {
|
if err := db.Append(signature.CERT_X509_GUID, *efiGUID, enrolledCertificate); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign the ESL, but for each EFI variable
|
if options.IncludeWellKnownCertificates {
|
||||||
|
owner := util.StringToGUID(microsoftSignatureOwnerGUID)
|
||||||
|
for _, cert := range wellKnownDBCertificates() {
|
||||||
|
if err := db.Append(signature.CERT_X509_GUID, *owner, cert.Raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, signedDB, err := signature.SignEFIVariable(efivar.Db, db, signer.Signer(), signer.Certificate())
|
_, signedDB, err := signature.SignEFIVariable(efivar.Db, db, signer.Signer(), signer.Certificate())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, signedKEK, err := signature.SignEFIVariable(efivar.KEK, db, signer.Signer(), signer.Certificate())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, signedPK, err := signature.SignEFIVariable(efivar.PK, db, signer.Signer(), signer.Certificate())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return []Entry{
|
return []Entry{
|
||||||
{Name: constants.SignatureKeyAsset, Contents: signedDB.Bytes()},
|
{Name: constants.SignatureKeyAsset, Contents: signedDB.Bytes()},
|
||||||
{Name: constants.KeyExchangeKeyAsset, Contents: signedKEK.Bytes()},
|
{Name: constants.KeyExchangeKeyAsset, Contents: signedKEK.Bytes()},
|
||||||
|
|||||||
@ -133,7 +133,7 @@ func (i *Imager) outISO(ctx context.Context, path string, report *reporter.Repor
|
|||||||
|
|
||||||
var entries []database.Entry
|
var entries []database.Entry
|
||||||
|
|
||||||
entries, err = database.Generate(enrolledPEM, signer)
|
entries, err = database.Generate(enrolledPEM, signer, database.IncludeWellKnownCertificates(i.prof.Input.SecureBoot.IncludeWellKnownCerts))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to generate database: %w", err)
|
return fmt.Errorf("failed to generate database: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,8 @@ type SecureBootAssets struct {
|
|||||||
PlatformKeyPath string `yaml:"platformKeyPath,omitempty"`
|
PlatformKeyPath string `yaml:"platformKeyPath,omitempty"`
|
||||||
KeyExchangeKeyPath string `yaml:"keyExchangeKeyPath,omitempty"`
|
KeyExchangeKeyPath string `yaml:"keyExchangeKeyPath,omitempty"`
|
||||||
SignatureKeyPath string `yaml:"signatureKeyPath,omitempty"`
|
SignatureKeyPath string `yaml:"signatureKeyPath,omitempty"`
|
||||||
|
// Optional, auto-enrollment include well-known UEFI (Microsoft) certs.
|
||||||
|
IncludeWellKnownCerts bool `yaml:"includeWellKnownCerts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SigningKeyAndCertificate describes a signing key & certificate.
|
// SigningKeyAndCertificate describes a signing key & certificate.
|
||||||
|
|||||||
@ -1535,10 +1535,11 @@ talosctl gen secureboot database [flags]
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
```
|
```
|
||||||
--enrolled-certificate string path to the certificate to enroll (default "_out/uki-signing-cert.pem")
|
--enrolled-certificate string path to the certificate to enroll (default "_out/uki-signing-cert.pem")
|
||||||
-h, --help help for database
|
-h, --help help for database
|
||||||
--signing-certificate string path to the certificate used to sign the database (default "_out/uki-signing-cert.pem")
|
--include-well-known-uefi-certs include well-known UEFI (Microsoft) certificates in the database
|
||||||
--signing-key string path to the key used to sign the database (default "_out/uki-signing-key.pem")
|
--signing-certificate string path to the certificate used to sign the database (default "_out/uki-signing-cert.pem")
|
||||||
|
--signing-key string path to the key used to sign the database (default "_out/uki-signing-key.pem")
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user