diff --git a/cmd/flux/bootstrap.go b/cmd/flux/bootstrap.go
index ba84a91dd84edfc1f5eb4e461c5ab07bfc64bdc4..46d7fe38ad889efc5ecb51f7d051de77c20756b1 100644
--- a/cmd/flux/bootstrap.go
+++ b/cmd/flux/bootstrap.go
@@ -17,26 +17,15 @@ limitations under the License.
 package main
 
 import (
-	"context"
+	"crypto/elliptic"
 	"fmt"
-	"path/filepath"
-	"time"
+	"io/ioutil"
 
 	"github.com/spf13/cobra"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/wait"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
-	sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
 
 	"github.com/fluxcd/flux2/internal/flags"
 	"github.com/fluxcd/flux2/internal/utils"
-	"github.com/fluxcd/flux2/pkg/manifestgen/install"
-	kus "github.com/fluxcd/flux2/pkg/manifestgen/kustomization"
-	"github.com/fluxcd/flux2/pkg/manifestgen/sync"
-	"github.com/fluxcd/flux2/pkg/status"
+	"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
 )
 
 var bootstrapCmd = &cobra.Command{
@@ -46,21 +35,36 @@ var bootstrapCmd = &cobra.Command{
 }
 
 type bootstrapFlags struct {
-	version            string
+	version  string
+	arch     flags.Arch
+	logLevel flags.LogLevel
+
+	branch        string
+	manifestsPath string
+
 	defaultComponents  []string
 	extraComponents    []string
-	registry           string
-	imagePullSecret    string
-	branch             string
+	requiredComponents []string
+
+	registry        string
+	imagePullSecret string
+
+	secretName     string
+	tokenAuth      bool
+	keyAlgorithm   flags.PublicKeyAlgorithm
+	keyRSABits     flags.RSAKeyBits
+	keyECDSACurve  flags.ECDSACurve
+	sshHostname    string
+	caFile         string
+	privateKeyFile string
+
 	watchAllNamespaces bool
 	networkPolicy      bool
-	manifestsPath      string
-	arch               flags.Arch
-	logLevel           flags.LogLevel
-	requiredComponents []string
-	tokenAuth          bool
 	clusterDomain      string
 	tolerationKeys     []string
+
+	authorName  string
+	authorEmail string
 }
 
 const (
@@ -72,17 +76,21 @@ var bootstrapArgs = NewBootstrapFlags()
 func init() {
 	bootstrapCmd.PersistentFlags().StringVarP(&bootstrapArgs.version, "version", "v", "",
 		"toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases")
+
 	bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.defaultComponents, "components", rootArgs.defaults.Components,
 		"list of components, accepts comma-separated values")
 	bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.extraComponents, "components-extra", nil,
 		"list of components in addition to those supplied or defaulted, accepts comma-separated values")
+
 	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd",
 		"container registry where the toolkit images are published")
 	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.imagePullSecret, "image-pull-secret", "",
 		"Kubernetes secret name used for pulling the toolkit images from a private registry")
-	bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.arch, "arch", bootstrapArgs.arch.Description())
+
 	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch,
 		"default branch (for GitHub this must match the default branch setting for the organization)")
+	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory")
+
 	bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.watchAllNamespaces, "watch-all-namespaces", true,
 		"watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed")
 	bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.networkPolicy, "network-policy", true,
@@ -90,12 +98,25 @@ func init() {
 	bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.tokenAuth, "token-auth", false,
 		"when enabled, the personal access token will be used instead of SSH deploy key")
 	bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.logLevel, "log-level", bootstrapArgs.logLevel.Description())
-	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory")
 	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain")
 	bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.tolerationKeys, "toleration-keys", nil,
 		"list of toleration keys used to schedule the components pods onto nodes with matching taints")
-	bootstrapCmd.PersistentFlags().MarkHidden("manifests")
+
+	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.secretName, "secret-name", rootArgs.defaults.Namespace, "name of the secret the sync credentials can be found in or stored to")
+	bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyAlgorithm, "ssh-key-algorithm", bootstrapArgs.keyAlgorithm.Description())
+	bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyRSABits, "ssh-rsa-bits", bootstrapArgs.keyRSABits.Description())
+	bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyECDSACurve, "ssh-ecdsa-curve", bootstrapArgs.keyECDSACurve.Description())
+	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshHostname, "ssh-hostname", "", "SSH hostname, to be used when the SSH host differs from the HTTPS one")
+	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates")
+	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.privateKeyFile, "private-key-file", "", "path to a private key file used for authenticating to the Git SSH server")
+
+	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorName, "author-name", "Flux", "author name for Git commits")
+	bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorEmail, "author-email", "", "author email for Git commits")
+
+	bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.arch, "arch", bootstrapArgs.arch.Description())
 	bootstrapCmd.PersistentFlags().MarkDeprecated("arch", "multi-arch container image is now available for AMD64, ARMv7 and ARM64")
+	bootstrapCmd.PersistentFlags().MarkHidden("manifests")
+
 	rootCmd.AddCommand(bootstrapCmd)
 }
 
@@ -103,6 +124,9 @@ func NewBootstrapFlags() bootstrapFlags {
 	return bootstrapFlags{
 		logLevel:           flags.LogLevel(rootArgs.defaults.LogLevel),
 		requiredComponents: []string{"source-controller", "kustomize-controller"},
+		keyAlgorithm:       flags.PublicKeyAlgorithm(sourcesecret.RSAPrivateKeyAlgorithm),
+		keyRSABits:         2048,
+		keyECDSACurve:      flags.ECDSACurve{Curve: elliptic.P384()},
 	}
 }
 
@@ -110,194 +134,39 @@ func bootstrapComponents() []string {
 	return append(bootstrapArgs.defaultComponents, bootstrapArgs.extraComponents...)
 }
 
-func bootstrapValidate() error {
-	components := bootstrapComponents()
-	for _, component := range bootstrapArgs.requiredComponents {
-		if !utils.ContainsItemString(components, component) {
-			return fmt.Errorf("component %s is required", component)
-		}
-	}
-
-	if err := utils.ValidateComponents(components); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func generateInstallManifests(targetPath, namespace, tmpDir string, localManifests string) (string, error) {
-	if ver, err := getVersion(bootstrapArgs.version); err != nil {
-		return "", err
-	} else {
-		bootstrapArgs.version = ver
-	}
-
-	manifestsBase := ""
-	if isEmbeddedVersion(bootstrapArgs.version) {
-		if err := writeEmbeddedManifests(tmpDir); err != nil {
-			return "", err
-		}
-		manifestsBase = tmpDir
-	}
-
-	opts := install.Options{
-		BaseURL:                localManifests,
-		Version:                bootstrapArgs.version,
-		Namespace:              namespace,
-		Components:             bootstrapComponents(),
-		Registry:               bootstrapArgs.registry,
-		ImagePullSecret:        bootstrapArgs.imagePullSecret,
-		WatchAllNamespaces:     bootstrapArgs.watchAllNamespaces,
-		NetworkPolicy:          bootstrapArgs.networkPolicy,
-		LogLevel:               bootstrapArgs.logLevel.String(),
-		NotificationController: rootArgs.defaults.NotificationController,
-		ManifestFile:           rootArgs.defaults.ManifestFile,
-		Timeout:                rootArgs.timeout,
-		TargetPath:             targetPath,
-		ClusterDomain:          bootstrapArgs.clusterDomain,
-		TolerationKeys:         bootstrapArgs.tolerationKeys,
-	}
-
-	if localManifests == "" {
-		opts.BaseURL = rootArgs.defaults.BaseURL
-	}
-
-	output, err := install.Generate(opts, manifestsBase)
-	if err != nil {
-		return "", fmt.Errorf("generating install manifests failed: %w", err)
-	}
-
-	filePath, err := output.WriteFile(tmpDir)
-	if err != nil {
-		return "", fmt.Errorf("generating install manifests failed: %w", err)
-	}
-	return filePath, nil
-}
-
-func applyInstallManifests(ctx context.Context, manifestPath string, components []string) error {
-	kubectlArgs := []string{"apply", "-f", manifestPath}
-	if _, err := utils.ExecKubectlCommand(ctx, utils.ModeOS, rootArgs.kubeconfig, rootArgs.kubecontext, kubectlArgs...); err != nil {
-		return fmt.Errorf("install failed: %w", err)
-	}
-	kubeConfig, err := utils.KubeConfig(rootArgs.kubeconfig, rootArgs.kubecontext)
-	if err != nil {
-		return fmt.Errorf("install failed: %w", err)
-	}
-	statusChecker, err := status.NewStatusChecker(kubeConfig, time.Second, rootArgs.timeout, logger)
-	if err != nil {
-		return fmt.Errorf("install failed: %w", err)
-	}
-	componentRefs, err := buildComponentObjectRefs(components...)
-	if err != nil {
-		return fmt.Errorf("install failed: %w", err)
-	}
-	logger.Waitingf("verifying installation")
-	if err := statusChecker.Assess(componentRefs...); err != nil {
-		return fmt.Errorf("install failed")
-	}
-	return nil
-}
-
-func generateSyncManifests(url, branch, name, namespace, targetPath, tmpDir string, interval time.Duration) (string, error) {
-	opts := sync.Options{
-		Name:         name,
-		Namespace:    namespace,
-		URL:          url,
-		Branch:       branch,
-		Interval:     interval,
-		Secret:       namespace,
-		TargetPath:   targetPath,
-		ManifestFile: sync.MakeDefaultOptions().ManifestFile,
+func buildEmbeddedManifestBase() (string, error) {
+	if !isEmbeddedVersion(bootstrapArgs.version) {
+		return "", nil
 	}
-
-	manifest, err := sync.Generate(opts)
-	if err != nil {
-		return "", fmt.Errorf("generating install manifests failed: %w", err)
-	}
-
-	output, err := manifest.WriteFile(tmpDir)
+	tmpBaseDir, err := ioutil.TempDir("", "flux-manifests-")
 	if err != nil {
 		return "", err
 	}
-	outputDir := filepath.Dir(output)
-
-	kusOpts := kus.MakeDefaultOptions()
-	kusOpts.BaseDir = tmpDir
-	kusOpts.TargetPath = filepath.Dir(manifest.Path)
-
-	kustomization, err := kus.Generate(kusOpts)
-	if err != nil {
-		return "", err
-	}
-	if _, err = kustomization.WriteFile(tmpDir); err != nil {
+	if err := writeEmbeddedManifests(tmpBaseDir); err != nil {
 		return "", err
 	}
-
-	return outputDir, nil
+	return tmpBaseDir, nil
 }
 
-func applySyncManifests(ctx context.Context, kubeClient client.Client, name, namespace, manifestsPath string) error {
-	kubectlArgs := []string{"apply", "-k", manifestsPath}
-	if _, err := utils.ExecKubectlCommand(ctx, utils.ModeStderrOS, rootArgs.kubeconfig, rootArgs.kubecontext, kubectlArgs...); err != nil {
-		return err
-	}
-
-	logger.Waitingf("waiting for cluster sync")
-
-	var gitRepository sourcev1.GitRepository
-	if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
-		isGitRepositoryReady(ctx, kubeClient, types.NamespacedName{Name: name, Namespace: namespace}, &gitRepository)); err != nil {
-		return err
+func bootstrapValidate() error {
+	components := bootstrapComponents()
+	for _, component := range bootstrapArgs.requiredComponents {
+		if !utils.ContainsItemString(components, component) {
+			return fmt.Errorf("component %s is required", component)
+		}
 	}
 
-	var kustomization kustomizev1.Kustomization
-	if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
-		isKustomizationReady(ctx, kubeClient, types.NamespacedName{Name: name, Namespace: namespace}, &kustomization)); err != nil {
+	if err := utils.ValidateComponents(components); err != nil {
 		return err
 	}
 
 	return nil
 }
 
-func shouldInstallManifests(ctx context.Context, kubeClient client.Client, namespace string) bool {
-	namespacedName := types.NamespacedName{
-		Namespace: namespace,
-		Name:      namespace,
-	}
-	var kustomization kustomizev1.Kustomization
-	if err := kubeClient.Get(ctx, namespacedName, &kustomization); err != nil {
-		return true
+func mapTeamSlice(s []string, defaultPermission string) map[string]string {
+	m := make(map[string]string, len(s))
+	for _, v := range s {
+		m[v] = defaultPermission
 	}
-
-	return kustomization.Status.LastAppliedRevision == ""
-}
-
-func shouldCreateDeployKey(ctx context.Context, kubeClient client.Client, namespace string) bool {
-	namespacedName := types.NamespacedName{
-		Namespace: namespace,
-		Name:      namespace,
-	}
-
-	var existing corev1.Secret
-	if err := kubeClient.Get(ctx, namespacedName, &existing); err != nil {
-		return true
-	}
-	return false
-}
-
-func checkIfBootstrapPathDiffers(ctx context.Context, kubeClient client.Client, namespace string, path string) (string, bool) {
-	namespacedName := types.NamespacedName{
-		Name:      namespace,
-		Namespace: namespace,
-	}
-	var fluxSystemKustomization kustomizev1.Kustomization
-	err := kubeClient.Get(ctx, namespacedName, &fluxSystemKustomization)
-	if err != nil {
-		return "", false
-	}
-	if fluxSystemKustomization.Spec.Path == path {
-		return "", false
-	}
-
-	return fluxSystemKustomization.Spec.Path, true
+	return m
 }
diff --git a/cmd/flux/bootstrap_github.go b/cmd/flux/bootstrap_github.go
index 7ccd49079056d40878b79644c208880fe5295b75..747d9829824db769de2f6cf1fd5effc6f2f56e45 100644
--- a/cmd/flux/bootstrap_github.go
+++ b/cmd/flux/bootstrap_github.go
@@ -20,20 +20,20 @@ import (
 	"context"
 	"fmt"
 	"io/ioutil"
-	"net/url"
 	"os"
-	"path"
-	"path/filepath"
 	"time"
 
-	"github.com/fluxcd/pkg/git"
+	"github.com/go-git/go-git/v5/plumbing/transport/http"
 	"github.com/spf13/cobra"
-	corev1 "k8s.io/api/core/v1"
-	"sigs.k8s.io/yaml"
 
+	"github.com/fluxcd/flux2/internal/bootstrap"
+	"github.com/fluxcd/flux2/internal/bootstrap/git/gogit"
+	"github.com/fluxcd/flux2/internal/bootstrap/provider"
 	"github.com/fluxcd/flux2/internal/flags"
 	"github.com/fluxcd/flux2/internal/utils"
+	"github.com/fluxcd/flux2/pkg/manifestgen/install"
 	"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
+	"github.com/fluxcd/flux2/pkg/manifestgen/sync"
 )
 
 var bootstrapGitHubCmd = &cobra.Command{
@@ -71,19 +71,21 @@ the bootstrap command will perform an upgrade if needed.`,
 }
 
 type githubFlags struct {
-	owner       string
-	repository  string
-	interval    time.Duration
-	personal    bool
-	private     bool
-	hostname    string
-	path        flags.SafeRelativePath
-	teams       []string
-	sshHostname string
+	owner        string
+	repository   string
+	interval     time.Duration
+	personal     bool
+	private      bool
+	hostname     string
+	path         flags.SafeRelativePath
+	teams        []string
+	readWriteKey bool
 }
 
 const (
 	ghDefaultPermission = "maintain"
+	ghDefaultDomain     = "github.com"
+	ghTokenEnvVar       = "GITHUB_TOKEN"
 )
 
 var githubArgs githubFlags
@@ -95,17 +97,17 @@ func init() {
 	bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.personal, "personal", false, "if true, the owner is assumed to be a GitHub user; otherwise an org")
 	bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.private, "private", true, "if true, the repository is assumed to be private")
 	bootstrapGitHubCmd.Flags().DurationVar(&githubArgs.interval, "interval", time.Minute, "sync interval")
-	bootstrapGitHubCmd.Flags().StringVar(&githubArgs.hostname, "hostname", git.GitHubDefaultHostname, "GitHub hostname")
-	bootstrapGitHubCmd.Flags().StringVar(&githubArgs.sshHostname, "ssh-hostname", "", "GitHub SSH hostname, to be used when the SSH host differs from the HTTPS one")
+	bootstrapGitHubCmd.Flags().StringVar(&githubArgs.hostname, "hostname", ghDefaultDomain, "GitHub hostname")
 	bootstrapGitHubCmd.Flags().Var(&githubArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
+	bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions")
 
 	bootstrapCmd.AddCommand(bootstrapGitHubCmd)
 }
 
 func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
-	ghToken := os.Getenv(git.GitHubTokenName)
+	ghToken := os.Getenv(ghTokenEnvVar)
 	if ghToken == "" {
-		return fmt.Errorf("%s environment variable not found", git.GitHubTokenName)
+		return fmt.Errorf("%s environment variable not found", ghTokenEnvVar)
 	}
 
 	if err := bootstrapValidate(); err != nil {
@@ -120,205 +122,124 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	usedPath, bootstrapPathDiffers := checkIfBootstrapPathDiffers(
-		ctx,
-		kubeClient,
-		rootArgs.namespace,
-		filepath.ToSlash(githubArgs.path.String()),
-	)
-
-	if bootstrapPathDiffers {
-		return fmt.Errorf("cluster already bootstrapped to %v path", usedPath)
+	// Manifest base
+	if ver, err := getVersion(bootstrapArgs.version); err == nil {
+		bootstrapArgs.version = ver
 	}
-
-	repository, err := git.NewRepository(
-		githubArgs.repository,
-		githubArgs.owner,
-		githubArgs.hostname,
-		ghToken,
-		"flux",
-		githubArgs.owner+"@users.noreply.github.com",
-	)
+	manifestsBase, err := buildEmbeddedManifestBase()
 	if err != nil {
 		return err
 	}
+	defer os.RemoveAll(manifestsBase)
 
-	if githubArgs.sshHostname != "" {
-		repository.SSHHost = githubArgs.sshHostname
-	}
-
-	provider := &git.GithubProvider{
-		IsPrivate:  githubArgs.private,
-		IsPersonal: githubArgs.personal,
+	// Build GitHub provider
+	providerCfg := provider.Config{
+		Provider: provider.GitProviderGitHub,
+		Hostname: githubArgs.hostname,
+		Token:    ghToken,
 	}
-
-	tmpDir, err := ioutil.TempDir("", rootArgs.namespace)
+	providerClient, err := provider.BuildGitProvider(providerCfg)
 	if err != nil {
 		return err
 	}
-	defer os.RemoveAll(tmpDir)
 
-	// create GitHub repository if doesn't exists
-	logger.Actionf("connecting to %s", githubArgs.hostname)
-	changed, err := provider.CreateRepository(ctx, repository)
+	// Lazy go-git repository
+	tmpDir, err := ioutil.TempDir("", "flux-bootstrap-")
 	if err != nil {
-		return err
-	}
-	if changed {
-		logger.Successf("repository created")
-	}
-
-	withErrors := false
-	// add teams to org repository
-	if !githubArgs.personal {
-		for _, team := range githubArgs.teams {
-			if changed, err := provider.AddTeam(ctx, repository, team, ghDefaultPermission); err != nil {
-				logger.Failuref(err.Error())
-				withErrors = true
-			} else if changed {
-				logger.Successf("%s team access granted", team)
-			}
-		}
-	}
-
-	// clone repository and checkout the main branch
-	if err := repository.Checkout(ctx, bootstrapArgs.branch, tmpDir); err != nil {
-		return err
-	}
-	logger.Successf("repository cloned")
-
-	// generate install manifests
-	logger.Generatef("generating manifests")
-	installManifest, err := generateInstallManifests(
-		githubArgs.path.String(),
-		rootArgs.namespace,
-		tmpDir,
-		bootstrapArgs.manifestsPath,
-	)
-	if err != nil {
-		return err
-	}
-
-	// stage install manifests
-	changed, err = repository.Commit(
-		ctx,
-		path.Join(githubArgs.path.String(), rootArgs.namespace),
-		fmt.Sprintf("Add flux %s components manifests", bootstrapArgs.version),
-	)
-	if err != nil {
-		return err
-	}
-
-	// push install manifests
-	if changed {
-		if err := repository.Push(ctx); err != nil {
-			return err
-		}
-		logger.Successf("components manifests pushed")
-	} else {
-		logger.Successf("components are up to date")
+		return fmt.Errorf("failed to create temporary working dir: %w", err)
 	}
-
-	// determine if repository synchronization is working
-	isInstall := shouldInstallManifests(ctx, kubeClient, rootArgs.namespace)
-
-	if isInstall {
-		// apply install manifests
-		logger.Actionf("installing components in %s namespace", rootArgs.namespace)
-		if err := applyInstallManifests(ctx, installManifest, bootstrapComponents()); err != nil {
-			return err
-		}
-		logger.Successf("install completed")
-	}
-
-	repoURL := repository.GetSSH()
+	defer os.RemoveAll(tmpDir)
+	gitClient := gogit.New(tmpDir, &http.BasicAuth{
+		Username: githubArgs.owner,
+		Password: ghToken,
+	})
+
+	// Install manifest config
+	installOptions := install.Options{
+		BaseURL:                rootArgs.defaults.BaseURL,
+		Version:                bootstrapArgs.version,
+		Namespace:              rootArgs.namespace,
+		Components:             bootstrapComponents(),
+		Registry:               bootstrapArgs.registry,
+		ImagePullSecret:        bootstrapArgs.imagePullSecret,
+		WatchAllNamespaces:     bootstrapArgs.watchAllNamespaces,
+		NetworkPolicy:          bootstrapArgs.networkPolicy,
+		LogLevel:               bootstrapArgs.logLevel.String(),
+		NotificationController: rootArgs.defaults.NotificationController,
+		ManifestFile:           rootArgs.defaults.ManifestFile,
+		Timeout:                rootArgs.timeout,
+		TargetPath:             githubArgs.path.String(),
+		ClusterDomain:          bootstrapArgs.clusterDomain,
+		TolerationKeys:         bootstrapArgs.tolerationKeys,
+	}
+	if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" {
+		installOptions.BaseURL = customBaseURL
+	}
+
+	// Source generation and secret config
 	secretOpts := sourcesecret.Options{
-		Name:      rootArgs.namespace,
-		Namespace: rootArgs.namespace,
+		Name:         bootstrapArgs.secretName,
+		Namespace:    rootArgs.namespace,
+		TargetPath:   githubArgs.path.String(),
+		ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
 	}
 	if bootstrapArgs.tokenAuth {
-		// Setup HTTPS token auth
-		repoURL = repository.GetURL()
 		secretOpts.Username = "git"
 		secretOpts.Password = ghToken
-	} else if shouldCreateDeployKey(ctx, kubeClient, rootArgs.namespace) {
-		// Setup SSH auth
-		u, err := url.Parse(repoURL)
-		if err != nil {
-			return fmt.Errorf("git URL parse failed: %w", err)
-		}
-		secretOpts.SSHHostname = u.Host
-		secretOpts.PrivateKeyAlgorithm = sourcesecret.RSAPrivateKeyAlgorithm
-		secretOpts.RSAKeyBits = 2048
-	}
 
-	secret, err := sourcesecret.Generate(secretOpts)
-	if err != nil {
-		return err
-	}
-	var s corev1.Secret
-	if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
-		return err
-	}
-	if len(s.StringData) > 0 {
-		logger.Actionf("configuring deploy key")
-		if err := upsertSecret(ctx, kubeClient, s); err != nil {
-			return err
+		if bootstrapArgs.caFile != "" {
+			secretOpts.CAFilePath = bootstrapArgs.caFile
 		}
+	} else {
+		secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
+		secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
+		secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
+		secretOpts.SSHHostname = githubArgs.hostname
 
-		if ppk, ok := s.StringData[sourcesecret.PublicKeySecretKey]; ok {
-			keyName := "flux"
-			if githubArgs.path != "" {
-				keyName = fmt.Sprintf("flux-%s", githubArgs.path)
-			}
-
-			if changed, err := provider.AddDeployKey(ctx, repository, ppk, keyName); err != nil {
-				return err
-			} else if changed {
-				logger.Successf("deploy key configured")
-			}
+		if bootstrapArgs.sshHostname != "" {
+			secretOpts.SSHHostname = bootstrapArgs.sshHostname
 		}
 	}
 
-	// configure repository synchronization
-	logger.Actionf("generating sync manifests")
-	syncManifests, err := generateSyncManifests(
-		repoURL,
-		bootstrapArgs.branch,
-		rootArgs.namespace,
-		rootArgs.namespace,
-		filepath.ToSlash(githubArgs.path.String()),
-		tmpDir,
-		githubArgs.interval,
-	)
-	if err != nil {
-		return err
+	// Sync manifest config
+	syncOpts := sync.Options{
+		Interval:          githubArgs.interval,
+		Name:              rootArgs.namespace,
+		Namespace:         rootArgs.namespace,
+		Branch:            bootstrapArgs.branch,
+		Secret:            bootstrapArgs.secretName,
+		TargetPath:        githubArgs.path.String(),
+		ManifestFile:      sync.MakeDefaultOptions().ManifestFile,
+		GitImplementation: sourceGitArgs.gitImplementation.String(),
+	}
+
+	// Bootstrap config
+	bootstrapOpts := []bootstrap.GitProviderOption{
+		bootstrap.WithProviderRepository(githubArgs.owner, githubArgs.repository, githubArgs.personal),
+		bootstrap.WithBranch(bootstrapArgs.branch),
+		bootstrap.WithBootstrapTransportType("https"),
+		bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
+		bootstrap.WithProviderTeamPermissions(mapTeamSlice(githubArgs.teams, ghDefaultPermission)),
+		bootstrap.WithReadWriteKeyPermissions(githubArgs.readWriteKey),
+		bootstrap.WithKubeconfig(rootArgs.kubeconfig, rootArgs.kubecontext),
+		bootstrap.WithLogger(logger),
+	}
+	if bootstrapArgs.sshHostname != "" {
+		bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
 	}
-
-	// commit and push manifests
-	if changed, err = repository.Commit(
-		ctx,
-		path.Join(githubArgs.path.String(), rootArgs.namespace),
-		fmt.Sprintf("Add flux %s sync manifests", bootstrapArgs.version),
-	); err != nil {
-		return err
-	} else if changed {
-		if err := repository.Push(ctx); err != nil {
-			return err
-		}
-		logger.Successf("sync manifests pushed")
+	if bootstrapArgs.tokenAuth {
+		bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https"))
 	}
-
-	// apply manifests and waiting for sync
-	logger.Actionf("applying sync manifests")
-	if err := applySyncManifests(ctx, kubeClient, rootArgs.namespace, rootArgs.namespace, syncManifests); err != nil {
-		return err
+	if !githubArgs.private {
+		bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public"))
 	}
 
-	if withErrors {
-		return fmt.Errorf("bootstrap completed with errors")
+	// Setup bootstrapper with constructed configs
+	b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
+	if err != nil {
+		return err
 	}
 
-	logger.Successf("bootstrap finished")
-	return nil
+	// Run
+	return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout)
 }
diff --git a/cmd/flux/bootstrap_gitlab.go b/cmd/flux/bootstrap_gitlab.go
index f1f3655430a846b424a1bd878d62ea703d4e0cfb..48768c8009f2e188c888ab77b3535c0e81b171dc 100644
--- a/cmd/flux/bootstrap_gitlab.go
+++ b/cmd/flux/bootstrap_gitlab.go
@@ -20,22 +20,21 @@ import (
 	"context"
 	"fmt"
 	"io/ioutil"
-	"net/url"
 	"os"
-	"path"
-	"path/filepath"
 	"regexp"
 	"time"
 
+	"github.com/go-git/go-git/v5/plumbing/transport/http"
 	"github.com/spf13/cobra"
-	corev1 "k8s.io/api/core/v1"
-	"sigs.k8s.io/yaml"
-
-	"github.com/fluxcd/pkg/git"
 
+	"github.com/fluxcd/flux2/internal/bootstrap"
+	"github.com/fluxcd/flux2/internal/bootstrap/git/gogit"
+	"github.com/fluxcd/flux2/internal/bootstrap/provider"
 	"github.com/fluxcd/flux2/internal/flags"
 	"github.com/fluxcd/flux2/internal/utils"
+	"github.com/fluxcd/flux2/pkg/manifestgen/install"
 	"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret"
+	"github.com/fluxcd/flux2/pkg/manifestgen/sync"
 )
 
 var bootstrapGitLabCmd = &cobra.Command{
@@ -70,18 +69,22 @@ the bootstrap command will perform an upgrade if needed.`,
 }
 
 const (
-	gitlabProjectRegex = `\A[[:alnum:]\x{00A9}-\x{1f9ff}_][[:alnum:]\p{Pd}\x{00A9}-\x{1f9ff}_\.]*\z`
+	glDefaultPermission = "maintain"
+	glDefaultDomain     = "gitlab.com"
+	glTokenEnvVar       = "GITLAB_TOKEN"
+	gitlabProjectRegex  = `\A[[:alnum:]\x{00A9}-\x{1f9ff}_][[:alnum:]\p{Pd}\x{00A9}-\x{1f9ff}_\.]*\z`
 )
 
 type gitlabFlags struct {
-	owner       string
-	repository  string
-	interval    time.Duration
-	personal    bool
-	private     bool
-	hostname    string
-	sshHostname string
-	path        flags.SafeRelativePath
+	owner        string
+	repository   string
+	interval     time.Duration
+	personal     bool
+	private      bool
+	hostname     string
+	path         flags.SafeRelativePath
+	teams        []string
+	readWriteKey bool
 }
 
 var gitlabArgs gitlabFlags
@@ -89,29 +92,29 @@ var gitlabArgs gitlabFlags
 func init() {
 	bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.owner, "owner", "", "GitLab user or group name")
 	bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.repository, "repository", "", "GitLab repository name")
+	bootstrapGitLabCmd.Flags().StringArrayVar(&gitlabArgs.teams, "team", []string{}, "GitLab teams to be given maintainer access")
 	bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.personal, "personal", false, "if true, the owner is assumed to be a GitLab user; otherwise a group")
 	bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.private, "private", true, "if true, the repository is assumed to be private")
 	bootstrapGitLabCmd.Flags().DurationVar(&gitlabArgs.interval, "interval", time.Minute, "sync interval")
-	bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.hostname, "hostname", git.GitLabDefaultHostname, "GitLab hostname")
-	bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.sshHostname, "ssh-hostname", "", "GitLab SSH hostname, to be used when the SSH host differs from the HTTPS one")
+	bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.hostname, "hostname", glDefaultDomain, "GitLab hostname")
 	bootstrapGitLabCmd.Flags().Var(&gitlabArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
+	bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions")
 
 	bootstrapCmd.AddCommand(bootstrapGitLabCmd)
 }
 
 func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
-	glToken := os.Getenv(git.GitLabTokenName)
+	glToken := os.Getenv(glTokenEnvVar)
 	if glToken == "" {
-		return fmt.Errorf("%s environment variable not found", git.GitLabTokenName)
+		return fmt.Errorf("%s environment variable not found", glTokenEnvVar)
 	}
 
-	projectNameIsValid, err := regexp.MatchString(gitlabProjectRegex, gitlabArgs.repository)
-	if err != nil {
+	if projectNameIsValid, err := regexp.MatchString(gitlabProjectRegex, gitlabArgs.repository); err != nil || !projectNameIsValid {
+		if err == nil {
+			err = fmt.Errorf("%s is an invalid project name for gitlab.\nIt can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'.", gitlabArgs.repository)
+		}
 		return err
 	}
-	if !projectNameIsValid {
-		return fmt.Errorf("%s is an invalid project name for gitlab.\nIt can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'.", gitlabArgs.repository)
-	}
 
 	if err := bootstrapValidate(); err != nil {
 		return err
@@ -125,183 +128,127 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	usedPath, bootstrapPathDiffers := checkIfBootstrapPathDiffers(ctx, kubeClient, rootArgs.namespace, filepath.ToSlash(gitlabArgs.path.String()))
-
-	if bootstrapPathDiffers {
-		return fmt.Errorf("cluster already bootstrapped to %v path", usedPath)
+	// Manifest base
+	if ver, err := getVersion(bootstrapArgs.version); err == nil {
+		bootstrapArgs.version = ver
 	}
-
-	repository, err := git.NewRepository(
-		gitlabArgs.repository,
-		gitlabArgs.owner,
-		gitlabArgs.hostname,
-		glToken,
-		"flux",
-		gitlabArgs.owner+"@users.noreply.gitlab.com",
-	)
+	manifestsBase, err := buildEmbeddedManifestBase()
 	if err != nil {
 		return err
 	}
+	defer os.RemoveAll(manifestsBase)
 
-	if gitlabArgs.sshHostname != "" {
-		repository.SSHHost = gitlabArgs.sshHostname
+	// Build GitLab provider
+	providerCfg := provider.Config{
+		Provider: provider.GitProviderGitLab,
+		Hostname: gitlabArgs.hostname,
+		Token:    glToken,
 	}
-
-	tmpDir, err := ioutil.TempDir("", rootArgs.namespace)
+	providerClient, err := provider.BuildGitProvider(providerCfg)
 	if err != nil {
 		return err
 	}
-	defer os.RemoveAll(tmpDir)
-
-	provider := &git.GitLabProvider{
-		IsPrivate:  gitlabArgs.private,
-		IsPersonal: gitlabArgs.personal,
-	}
 
-	// create GitLab project if doesn't exists
-	logger.Actionf("connecting to %s", gitlabArgs.hostname)
-	changed, err := provider.CreateRepository(ctx, repository)
+	// Lazy go-git repository
+	tmpDir, err := ioutil.TempDir("", "flux-bootstrap-")
 	if err != nil {
-		return err
-	}
-	if changed {
-		logger.Successf("repository created")
+		return fmt.Errorf("failed to create temporary working dir: %w", err)
 	}
-
-	// clone repository and checkout the master branch
-	if err := repository.Checkout(ctx, bootstrapArgs.branch, tmpDir); err != nil {
-		return err
-	}
-	logger.Successf("repository cloned")
-
-	// generate install manifests
-	logger.Generatef("generating manifests")
-	installManifest, err := generateInstallManifests(
-		gitlabArgs.path.String(),
-		rootArgs.namespace,
-		tmpDir,
-		bootstrapArgs.manifestsPath,
-	)
-	if err != nil {
-		return err
-	}
-
-	// stage install manifests
-	changed, err = repository.Commit(
-		ctx,
-		path.Join(gitlabArgs.path.String(), rootArgs.namespace),
-		fmt.Sprintf("Add flux %s components manifests", bootstrapArgs.version),
-	)
-	if err != nil {
-		return err
-	}
-
-	// push install manifests
-	if changed {
-		if err := repository.Push(ctx); err != nil {
-			return err
-		}
-		logger.Successf("components manifests pushed")
-	} else {
-		logger.Successf("components are up to date")
-	}
-
-	// determine if repository synchronization is working
-	isInstall := shouldInstallManifests(ctx, kubeClient, rootArgs.namespace)
-
-	if isInstall {
-		// apply install manifests
-		logger.Actionf("installing components in %s namespace", rootArgs.namespace)
-		if err := applyInstallManifests(ctx, installManifest, bootstrapComponents()); err != nil {
-			return err
-		}
-		logger.Successf("install completed")
-	}
-
-	repoURL := repository.GetSSH()
+	defer os.RemoveAll(tmpDir)
+	gitClient := gogit.New(tmpDir, &http.BasicAuth{
+		Username: gitlabArgs.owner,
+		Password: glToken,
+	})
+
+	// Install manifest config
+	installOptions := install.Options{
+		BaseURL:                rootArgs.defaults.BaseURL,
+		Version:                bootstrapArgs.version,
+		Namespace:              rootArgs.namespace,
+		Components:             bootstrapComponents(),
+		Registry:               bootstrapArgs.registry,
+		ImagePullSecret:        bootstrapArgs.imagePullSecret,
+		WatchAllNamespaces:     bootstrapArgs.watchAllNamespaces,
+		NetworkPolicy:          bootstrapArgs.networkPolicy,
+		LogLevel:               bootstrapArgs.logLevel.String(),
+		NotificationController: rootArgs.defaults.NotificationController,
+		ManifestFile:           rootArgs.defaults.ManifestFile,
+		Timeout:                rootArgs.timeout,
+		TargetPath:             gitlabArgs.path.String(),
+		ClusterDomain:          bootstrapArgs.clusterDomain,
+		TolerationKeys:         bootstrapArgs.tolerationKeys,
+	}
+	if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" {
+		installOptions.BaseURL = customBaseURL
+	}
+
+	// Source generation and secret config
 	secretOpts := sourcesecret.Options{
-		Name:      rootArgs.namespace,
-		Namespace: rootArgs.namespace,
+		Name:         bootstrapArgs.secretName,
+		Namespace:    rootArgs.namespace,
+		TargetPath:   gitlabArgs.path.String(),
+		ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
 	}
 	if bootstrapArgs.tokenAuth {
-		// Setup HTTPS token auth
-		repoURL = repository.GetURL()
 		secretOpts.Username = "git"
 		secretOpts.Password = glToken
-	} else if shouldCreateDeployKey(ctx, kubeClient, rootArgs.namespace) {
-		// Setup SSH auth
-		u, err := url.Parse(repoURL)
-		if err != nil {
-			return fmt.Errorf("git URL parse failed: %w", err)
-		}
-		secretOpts.SSHHostname = u.Host
-		secretOpts.PrivateKeyAlgorithm = sourcesecret.RSAPrivateKeyAlgorithm
-		secretOpts.RSAKeyBits = 2048
-	}
 
-	secret, err := sourcesecret.Generate(secretOpts)
-	if err != nil {
-		return err
-	}
-	var s corev1.Secret
-	if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
-		return err
-	}
-	if len(s.StringData) > 0 {
-		logger.Actionf("configuring deploy key")
-		if err := upsertSecret(ctx, kubeClient, s); err != nil {
-			return err
+		if bootstrapArgs.caFile != "" {
+			secretOpts.CAFilePath = bootstrapArgs.caFile
 		}
+	} else {
+		secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
+		secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
+		secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
+		secretOpts.SSHHostname = githubArgs.hostname
 
-		if ppk, ok := s.StringData[sourcesecret.PublicKeySecretKey]; ok {
-			keyName := "flux"
-			if gitlabArgs.path != "" {
-				keyName = fmt.Sprintf("flux-%s", gitlabArgs.path)
-			}
-
-			if changed, err := provider.AddDeployKey(ctx, repository, ppk, keyName); err != nil {
-				return err
-			} else if changed {
-				logger.Successf("deploy key configured")
-			}
+		if bootstrapArgs.privateKeyFile != "" {
+			secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile
+		}
+		if bootstrapArgs.sshHostname != "" {
+			secretOpts.SSHHostname = bootstrapArgs.sshHostname
 		}
 	}
 
-	// configure repository synchronization
-	logger.Actionf("generating sync manifests")
-	syncManifests, err := generateSyncManifests(
-		repoURL,
-		bootstrapArgs.branch,
-		rootArgs.namespace,
-		rootArgs.namespace,
-		filepath.ToSlash(gitlabArgs.path.String()),
-		tmpDir,
-		gitlabArgs.interval,
-	)
-	if err != nil {
-		return err
+	// Sync manifest config
+	syncOpts := sync.Options{
+		Interval:          gitlabArgs.interval,
+		Name:              rootArgs.namespace,
+		Namespace:         rootArgs.namespace,
+		Branch:            bootstrapArgs.branch,
+		Secret:            bootstrapArgs.secretName,
+		TargetPath:        gitlabArgs.path.String(),
+		ManifestFile:      sync.MakeDefaultOptions().ManifestFile,
+		GitImplementation: sourceGitArgs.gitImplementation.String(),
+	}
+
+	// Bootstrap config
+	bootstrapOpts := []bootstrap.GitProviderOption{
+		bootstrap.WithProviderRepository(gitlabArgs.owner, gitlabArgs.repository, gitlabArgs.personal),
+		bootstrap.WithBranch(bootstrapArgs.branch),
+		bootstrap.WithBootstrapTransportType("https"),
+		bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
+		bootstrap.WithProviderTeamPermissions(mapTeamSlice(gitlabArgs.teams, glDefaultPermission)),
+		bootstrap.WithReadWriteKeyPermissions(gitlabArgs.readWriteKey),
+		bootstrap.WithKubeconfig(rootArgs.kubeconfig, rootArgs.kubecontext),
+		bootstrap.WithLogger(logger),
+	}
+	if bootstrapArgs.sshHostname != "" {
+		bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
 	}
-
-	// commit and push manifests
-	if changed, err = repository.Commit(
-		ctx,
-		path.Join(gitlabArgs.path.String(), rootArgs.namespace),
-		fmt.Sprintf("Add flux %s sync manifests", bootstrapArgs.version),
-	); err != nil {
-		return err
-	} else if changed {
-		if err := repository.Push(ctx); err != nil {
-			return err
-		}
-		logger.Successf("sync manifests pushed")
+	if bootstrapArgs.tokenAuth {
+		bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https"))
+	}
+	if !gitlabArgs.private {
+		bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public"))
 	}
 
-	// apply manifests and waiting for sync
-	logger.Actionf("applying sync manifests")
-	if err := applySyncManifests(ctx, kubeClient, rootArgs.namespace, rootArgs.namespace, syncManifests); err != nil {
+	// Setup bootstrapper with constructed configs
+	b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
+	if err != nil {
 		return err
 	}
 
-	logger.Successf("bootstrap finished")
-	return nil
+	// Run
+	return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout)
 }
diff --git a/docs/cmd/flux_bootstrap.md b/docs/cmd/flux_bootstrap.md
index 26dbec70c032a6f9108b8b4e9d5670cd71ab1d5d..78760620a143f17ff1657fea2b7f64adc5a241da 100644
--- a/docs/cmd/flux_bootstrap.md
+++ b/docs/cmd/flux_bootstrap.md
@@ -12,19 +12,28 @@ The bootstrap sub-commands bootstrap the toolkit components on the targeted Git
 ### Options
 
 ```
-      --branch string              default branch (for GitHub this must match the default branch setting for the organization) (default "main")
-      --cluster-domain string      internal cluster domain (default "cluster.local")
-      --components strings         list of components, accepts comma-separated values (default [source-controller,kustomize-controller,helm-controller,notification-controller])
-      --components-extra strings   list of components in addition to those supplied or defaulted, accepts comma-separated values
-  -h, --help                       help for bootstrap
-      --image-pull-secret string   Kubernetes secret name used for pulling the toolkit images from a private registry
-      --log-level logLevel         log level, available options are: (debug, info, error) (default info)
-      --network-policy             deny ingress access to the toolkit controllers from other namespaces using network policies (default true)
-      --registry string            container registry where the toolkit images are published (default "ghcr.io/fluxcd")
-      --token-auth                 when enabled, the personal access token will be used instead of SSH deploy key
-      --toleration-keys strings    list of toleration keys used to schedule the components pods onto nodes with matching taints
-  -v, --version string             toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases
-      --watch-all-namespaces       watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed (default true)
+      --author-email string                    author email for Git commits
+      --author-name string                     author name for Git commits (default "Flux")
+      --branch string                          default branch (for GitHub this must match the default branch setting for the organization) (default "main")
+      --ca-file string                         path to TLS CA file used for validating self-signed certificates
+      --cluster-domain string                  internal cluster domain (default "cluster.local")
+      --components strings                     list of components, accepts comma-separated values (default [source-controller,kustomize-controller,helm-controller,notification-controller])
+      --components-extra strings               list of components in addition to those supplied or defaulted, accepts comma-separated values
+  -h, --help                                   help for bootstrap
+      --image-pull-secret string               Kubernetes secret name used for pulling the toolkit images from a private registry
+      --log-level logLevel                     log level, available options are: (debug, info, error) (default info)
+      --network-policy                         deny ingress access to the toolkit controllers from other namespaces using network policies (default true)
+      --private-key-file string                path to a private key file used for authenticating to the Git SSH server
+      --registry string                        container registry where the toolkit images are published (default "ghcr.io/fluxcd")
+      --secret-name string                     name of the secret the sync credentials can be found in or stored to (default "flux-system")
+      --ssh-ecdsa-curve ecdsaCurve             SSH ECDSA public key curve (p256, p384, p521) (default p384)
+      --ssh-hostname string                    SSH hostname, to be used when the SSH host differs from the HTTPS one
+      --ssh-key-algorithm publicKeyAlgorithm   SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa)
+      --ssh-rsa-bits rsaKeyBits                SSH RSA public key bit size (multiplies of 8) (default 2048)
+      --token-auth                             when enabled, the personal access token will be used instead of SSH deploy key
+      --toleration-keys strings                list of toleration keys used to schedule the components pods onto nodes with matching taints
+  -v, --version string                         toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases
+      --watch-all-namespaces                   watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed (default true)
 ```
 
 ### Options inherited from parent commands
diff --git a/docs/cmd/flux_bootstrap_github.md b/docs/cmd/flux_bootstrap_github.md
index 80ed707e01ea39667c288248df67ad52d05393d5..cde0605dca14d4e4f180dd1316e487b85d9d898c 100644
--- a/docs/cmd/flux_bootstrap_github.md
+++ b/docs/cmd/flux_bootstrap_github.md
@@ -55,31 +55,40 @@ flux bootstrap github [flags]
       --path safeRelativePath   path relative to the repository root, when specified the cluster sync will be scoped to this path
       --personal                if true, the owner is assumed to be a GitHub user; otherwise an org
       --private                 if true, the repository is assumed to be private (default true)
+      --read-write-key          if true, the deploy key is configured with read/write permissions
       --repository string       GitHub repository name
-      --ssh-hostname string     GitHub SSH hostname, to be used when the SSH host differs from the HTTPS one
       --team stringArray        GitHub team to be given maintainer access
 ```
 
 ### Options inherited from parent commands
 
 ```
-      --branch string              default branch (for GitHub this must match the default branch setting for the organization) (default "main")
-      --cluster-domain string      internal cluster domain (default "cluster.local")
-      --components strings         list of components, accepts comma-separated values (default [source-controller,kustomize-controller,helm-controller,notification-controller])
-      --components-extra strings   list of components in addition to those supplied or defaulted, accepts comma-separated values
-      --context string             kubernetes context to use
-      --image-pull-secret string   Kubernetes secret name used for pulling the toolkit images from a private registry
-      --kubeconfig string          absolute path to the kubeconfig file
-      --log-level logLevel         log level, available options are: (debug, info, error) (default info)
-  -n, --namespace string           the namespace scope for this operation (default "flux-system")
-      --network-policy             deny ingress access to the toolkit controllers from other namespaces using network policies (default true)
-      --registry string            container registry where the toolkit images are published (default "ghcr.io/fluxcd")
-      --timeout duration           timeout for this operation (default 5m0s)
-      --token-auth                 when enabled, the personal access token will be used instead of SSH deploy key
-      --toleration-keys strings    list of toleration keys used to schedule the components pods onto nodes with matching taints
-      --verbose                    print generated objects
-  -v, --version string             toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases
-      --watch-all-namespaces       watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed (default true)
+      --author-email string                    author email for Git commits
+      --author-name string                     author name for Git commits (default "Flux")
+      --branch string                          default branch (for GitHub this must match the default branch setting for the organization) (default "main")
+      --ca-file string                         path to TLS CA file used for validating self-signed certificates
+      --cluster-domain string                  internal cluster domain (default "cluster.local")
+      --components strings                     list of components, accepts comma-separated values (default [source-controller,kustomize-controller,helm-controller,notification-controller])
+      --components-extra strings               list of components in addition to those supplied or defaulted, accepts comma-separated values
+      --context string                         kubernetes context to use
+      --image-pull-secret string               Kubernetes secret name used for pulling the toolkit images from a private registry
+      --kubeconfig string                      absolute path to the kubeconfig file
+      --log-level logLevel                     log level, available options are: (debug, info, error) (default info)
+  -n, --namespace string                       the namespace scope for this operation (default "flux-system")
+      --network-policy                         deny ingress access to the toolkit controllers from other namespaces using network policies (default true)
+      --private-key-file string                path to a private key file used for authenticating to the Git SSH server
+      --registry string                        container registry where the toolkit images are published (default "ghcr.io/fluxcd")
+      --secret-name string                     name of the secret the sync credentials can be found in or stored to (default "flux-system")
+      --ssh-ecdsa-curve ecdsaCurve             SSH ECDSA public key curve (p256, p384, p521) (default p384)
+      --ssh-hostname string                    SSH hostname, to be used when the SSH host differs from the HTTPS one
+      --ssh-key-algorithm publicKeyAlgorithm   SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa)
+      --ssh-rsa-bits rsaKeyBits                SSH RSA public key bit size (multiplies of 8) (default 2048)
+      --timeout duration                       timeout for this operation (default 5m0s)
+      --token-auth                             when enabled, the personal access token will be used instead of SSH deploy key
+      --toleration-keys strings                list of toleration keys used to schedule the components pods onto nodes with matching taints
+      --verbose                                print generated objects
+  -v, --version string                         toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases
+      --watch-all-namespaces                   watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed (default true)
 ```
 
 ### SEE ALSO
diff --git a/docs/cmd/flux_bootstrap_gitlab.md b/docs/cmd/flux_bootstrap_gitlab.md
index 16acf645fd90a8881aeed50539ac894fb3cd0536..3d0b9c43126848bd491228c10bcfbc77d30f388d 100644
--- a/docs/cmd/flux_bootstrap_gitlab.md
+++ b/docs/cmd/flux_bootstrap_gitlab.md
@@ -52,30 +52,40 @@ flux bootstrap gitlab [flags]
       --path safeRelativePath   path relative to the repository root, when specified the cluster sync will be scoped to this path
       --personal                if true, the owner is assumed to be a GitLab user; otherwise a group
       --private                 if true, the repository is assumed to be private (default true)
+      --read-write-key          if true, the deploy key is configured with read/write permissions
       --repository string       GitLab repository name
-      --ssh-hostname string     GitLab SSH hostname, to be used when the SSH host differs from the HTTPS one
+      --team stringArray        GitLab teams to be given maintainer access
 ```
 
 ### Options inherited from parent commands
 
 ```
-      --branch string              default branch (for GitHub this must match the default branch setting for the organization) (default "main")
-      --cluster-domain string      internal cluster domain (default "cluster.local")
-      --components strings         list of components, accepts comma-separated values (default [source-controller,kustomize-controller,helm-controller,notification-controller])
-      --components-extra strings   list of components in addition to those supplied or defaulted, accepts comma-separated values
-      --context string             kubernetes context to use
-      --image-pull-secret string   Kubernetes secret name used for pulling the toolkit images from a private registry
-      --kubeconfig string          absolute path to the kubeconfig file
-      --log-level logLevel         log level, available options are: (debug, info, error) (default info)
-  -n, --namespace string           the namespace scope for this operation (default "flux-system")
-      --network-policy             deny ingress access to the toolkit controllers from other namespaces using network policies (default true)
-      --registry string            container registry where the toolkit images are published (default "ghcr.io/fluxcd")
-      --timeout duration           timeout for this operation (default 5m0s)
-      --token-auth                 when enabled, the personal access token will be used instead of SSH deploy key
-      --toleration-keys strings    list of toleration keys used to schedule the components pods onto nodes with matching taints
-      --verbose                    print generated objects
-  -v, --version string             toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases
-      --watch-all-namespaces       watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed (default true)
+      --author-email string                    author email for Git commits
+      --author-name string                     author name for Git commits (default "Flux")
+      --branch string                          default branch (for GitHub this must match the default branch setting for the organization) (default "main")
+      --ca-file string                         path to TLS CA file used for validating self-signed certificates
+      --cluster-domain string                  internal cluster domain (default "cluster.local")
+      --components strings                     list of components, accepts comma-separated values (default [source-controller,kustomize-controller,helm-controller,notification-controller])
+      --components-extra strings               list of components in addition to those supplied or defaulted, accepts comma-separated values
+      --context string                         kubernetes context to use
+      --image-pull-secret string               Kubernetes secret name used for pulling the toolkit images from a private registry
+      --kubeconfig string                      absolute path to the kubeconfig file
+      --log-level logLevel                     log level, available options are: (debug, info, error) (default info)
+  -n, --namespace string                       the namespace scope for this operation (default "flux-system")
+      --network-policy                         deny ingress access to the toolkit controllers from other namespaces using network policies (default true)
+      --private-key-file string                path to a private key file used for authenticating to the Git SSH server
+      --registry string                        container registry where the toolkit images are published (default "ghcr.io/fluxcd")
+      --secret-name string                     name of the secret the sync credentials can be found in or stored to (default "flux-system")
+      --ssh-ecdsa-curve ecdsaCurve             SSH ECDSA public key curve (p256, p384, p521) (default p384)
+      --ssh-hostname string                    SSH hostname, to be used when the SSH host differs from the HTTPS one
+      --ssh-key-algorithm publicKeyAlgorithm   SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa)
+      --ssh-rsa-bits rsaKeyBits                SSH RSA public key bit size (multiplies of 8) (default 2048)
+      --timeout duration                       timeout for this operation (default 5m0s)
+      --token-auth                             when enabled, the personal access token will be used instead of SSH deploy key
+      --toleration-keys strings                list of toleration keys used to schedule the components pods onto nodes with matching taints
+      --verbose                                print generated objects
+  -v, --version string                         toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases
+      --watch-all-namespaces                   watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed (default true)
 ```
 
 ### SEE ALSO
diff --git a/go.mod b/go.mod
index eab2d3b57e03299fc3974cf58f3adc0e4abed86d..ffdbc1a7740424a24791fd5ded4caf728515b956 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,6 @@ require (
 	github.com/fluxcd/kustomize-controller/api v0.10.0
 	github.com/fluxcd/notification-controller/api v0.11.0
 	github.com/fluxcd/pkg/apis/meta v0.8.0
-	github.com/fluxcd/pkg/git v0.3.0
 	github.com/fluxcd/pkg/runtime v0.10.1
 	github.com/fluxcd/pkg/ssh v0.0.5
 	github.com/fluxcd/pkg/untar v0.0.5
@@ -24,6 +23,7 @@ require (
 	github.com/olekukonko/tablewriter v0.0.4
 	github.com/spf13/cobra v1.1.1
 	github.com/spf13/pflag v1.0.5
+	github.com/xanzy/go-gitlab v0.43.0 // indirect
 	golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
 	k8s.io/api v0.20.2
 	k8s.io/apiextensions-apiserver v0.20.2
diff --git a/go.sum b/go.sum
index 60fa4d6a65509baca291e84db37a3f9862a63041..aa8adafd5e9fd5c5f554e0a755c963f138090ef3 100644
--- a/go.sum
+++ b/go.sum
@@ -204,8 +204,6 @@ github.com/fluxcd/pkg/apis/kustomize v0.0.1 h1:TkA80R0GopRY27VJqzKyS6ifiKIAfwBd7
 github.com/fluxcd/pkg/apis/kustomize v0.0.1/go.mod h1:JAFPfnRmcrAoG1gNiA8kmEXsnOBuDyZ/F5X4DAQcVV0=
 github.com/fluxcd/pkg/apis/meta v0.8.0 h1:wqWpUsxhKHB1ZztcvOz+vnyhdKW9cWmjFp8Vci/XOdk=
 github.com/fluxcd/pkg/apis/meta v0.8.0/go.mod h1:yHuY8kyGHYz22I0jQzqMMGCcHViuzC/WPdo9Gisk8Po=
-github.com/fluxcd/pkg/git v0.3.0 h1:nrKZWZ/ymDevud3Wf1LEieO/QcNPnqz1/MrkZBFcg9o=
-github.com/fluxcd/pkg/git v0.3.0/go.mod h1:ZwG0iLOqNSyNw6lsPIAO+v6+BqqCXyV+r1Oq6Lm+slg=
 github.com/fluxcd/pkg/runtime v0.10.1 h1:NV0pe6lFzodKBIz0dT3xkoR0wJnTCicXwM/v/d5T0+Y=
 github.com/fluxcd/pkg/runtime v0.10.1/go.mod h1:JD0eZIn5xkTeHHQUWXSqJPIh/ecO0d0qrUKbSVHnpnw=
 github.com/fluxcd/pkg/ssh v0.0.5 h1:rnbFZ7voy2JBlUfMbfyqArX2FYaLNpDhccGFC3qW83A=
@@ -389,8 +387,6 @@ github.com/google/go-containerregistry v0.2.0 h1:cWFYx+kOkKdyOET0pcp7GMCmxj7da40
 github.com/google/go-containerregistry v0.2.0/go.mod h1:Ts3Wioz1r5ayWx8sS6vLcWltWcM1aqFjd/eVrkFhrWM=
 github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
 github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
-github.com/google/go-github/v33 v33.0.0 h1:qAf9yP0qc54ufQxzwv+u9H0tiVOnPJxo0lI/JXqw3ZM=
-github.com/google/go-github/v33 v33.0.0/go.mod h1:GMdDnVZY/2TsWgp/lkYnpSAh6TrzhANBBwm6k6TTEXg=
 github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=