1
0
mirror of synced 2026-03-02 03:26:57 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Sanskar Jaiswal
067180b5b2 rfc: add more details around token refreshing and caching
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2023-08-02 12:46:06 +05:30
Sanskar Jaiswal
e3f6b242ea rfc: add passswordless auth for git repos draft
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2023-07-31 22:34:05 +05:30
13 changed files with 225 additions and 85 deletions

View File

@@ -57,7 +57,7 @@ type checkFlags struct {
} }
var kubernetesConstraints = []string{ var kubernetesConstraints = []string{
">=1.25.0-0", ">=1.24.0-0",
} }
var checkArgs checkFlags var checkArgs checkFlags

View File

@@ -101,7 +101,7 @@ func createImageRepositoryRun(cmd *cobra.Command, args []string) error {
var repo = imagev1.ImageRepository{ var repo = imagev1.ImageRepository{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: objectName, Name: objectName,
Namespace: GetDesiredNamespace(kubeconfigArgs), Namespace: *kubeconfigArgs.Namespace,
Labels: labels, Labels: labels,
}, },
Spec: imagev1.ImageRepositorySpec{ Spec: imagev1.ImageRepositorySpec{

View File

@@ -91,12 +91,6 @@ func TestDiffKustomization(t *testing.T) {
objectFile: "./testdata/diff-kustomization/stringdata-sops-secret.yaml", objectFile: "./testdata/diff-kustomization/stringdata-sops-secret.yaml",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-stringdata-sops-secret.golden"), assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-stringdata-sops-secret.golden"),
}, },
{
name: "diff where kustomization file has multiple objects with the same name",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml",
objectFile: "",
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
},
} }
tmpl := map[string]string{ tmpl := map[string]string{

View File

@@ -146,11 +146,9 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error {
return err return err
} }
ns := GetDesiredNamespace(kubeconfigArgs)
var listOpts []client.ListOption var listOpts []client.ListOption
if !getArgs.allNamespaces { if !getArgs.allNamespaces {
listOpts = append(listOpts, client.InNamespace(ns)) listOpts = append(listOpts, client.InNamespace(*kubeconfigArgs.Namespace))
} }
if len(args) > 0 { if len(args) > 0 {
@@ -192,12 +190,12 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error {
logger.Failuref("%s object '%s' not found in %s namespace", logger.Failuref("%s object '%s' not found in %s namespace",
get.kind, get.kind,
args[0], args[0],
namespaceNameOrAny(getArgs.allNamespaces, ns), namespaceNameOrAny(getArgs.allNamespaces, *kubeconfigArgs.Namespace),
) )
} else if !getAll { } else if !getAll {
logger.Failuref("no %s objects found in %s namespace", logger.Failuref("no %s objects found in %s namespace",
get.kind, get.kind,
namespaceNameOrAny(getArgs.allNamespaces, ns), namespaceNameOrAny(getArgs.allNamespaces, *kubeconfigArgs.Namespace),
) )
} }
return nil return nil

View File

@@ -146,7 +146,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
opts := install.Options{ opts := install.Options{
BaseURL: installArgs.manifestsPath, BaseURL: installArgs.manifestsPath,
Version: installArgs.version, Version: installArgs.version,
Namespace: GetDesiredNamespace(kubeconfigArgs), Namespace: *kubeconfigArgs.Namespace,
Components: components, Components: components,
Registry: installArgs.registry, Registry: installArgs.registry,
ImagePullSecret: installArgs.imagePullSecret, ImagePullSecret: installArgs.imagePullSecret,
@@ -181,7 +181,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Successf("manifests build completed") logger.Successf("manifests build completed")
logger.Actionf("installing components in %s namespace", opts.Namespace) logger.Actionf("installing components in %s namespace", *kubeconfigArgs.Namespace)
applyOutput, err := utils.Apply(ctx, kubeconfigArgs, kubeclientOptions, tmpDir, filepath.Join(tmpDir, manifest.Path)) applyOutput, err := utils.Apply(ctx, kubeconfigArgs, kubeclientOptions, tmpDir, filepath.Join(tmpDir, manifest.Path))
if err != nil { if err != nil {

View File

@@ -105,10 +105,6 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
return fmt.Errorf("error getting namespace: %w", err) return fmt.Errorf("error getting namespace: %w", err)
} }
if ns == "" {
return nil
}
if e := validation.IsDNS1123Label(ns); len(e) > 0 { if e := validation.IsDNS1123Label(ns); len(e) > 0 {
return fmt.Errorf("namespace must be a valid DNS label: %q", ns) return fmt.Errorf("namespace must be a valid DNS label: %q", ns)
} }
@@ -144,6 +140,7 @@ func init() {
rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation") rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation")
rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects") rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects")
configureDefaultNamespace()
kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag
kubeconfigArgs.Timeout = nil // prevent AddFlags from configuring --request-timeout flag, we have --timeout instead kubeconfigArgs.Timeout = nil // prevent AddFlags from configuring --request-timeout flag, we have --timeout instead
kubeconfigArgs.AddFlags(rootCmd.PersistentFlags()) kubeconfigArgs.AddFlags(rootCmd.PersistentFlags())
@@ -201,10 +198,8 @@ func main() {
} }
} }
func GetDesiredNamespace(cfg *genericclioptions.ConfigFlags) string { func configureDefaultNamespace() {
if *cfg.Namespace != "" { *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
return *cfg.Namespace
}
fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE") fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE")
if fromEnv != "" { if fromEnv != "" {
// namespace must be a valid DNS label. Assess against validation // namespace must be a valid DNS label. Assess against validation
@@ -212,28 +207,11 @@ func GetDesiredNamespace(cfg *genericclioptions.ConfigFlags) string {
// may not be actively provided by end-user. // may not be actively provided by end-user.
if e := validation.IsDNS1123Label(fromEnv); len(e) > 0 { if e := validation.IsDNS1123Label(fromEnv); len(e) > 0 {
logger.Warningf(" ignoring invalid FLUX_SYSTEM_NAMESPACE: %q", fromEnv) logger.Warningf(" ignoring invalid FLUX_SYSTEM_NAMESPACE: %q", fromEnv)
} else { return
return fromEnv
} }
}
if _, has := os.LookupEnv("FLUX_NS_FOLLOW_KUBECONTEXT"); has { kubeconfigArgs.Namespace = &fromEnv
rawCfg, err := cfg.ToRawKubeConfigLoader().RawConfig()
if err != nil {
logger.Warningf(" failed parsing kubeconfig, ignoring: %q", fromEnv)
} else {
ctx := *cfg.Context
if ctx == "" {
ctx = rawCfg.CurrentContext
}
ns := rawCfg.Contexts[ctx].Namespace
if ns != "" {
return ns
}
}
} }
return rootArgs.defaults.Namespace
} }
// readPasswordFromStdin reads a password from stdin and returns the input // readPasswordFromStdin reads a password from stdin and returns the input

View File

@@ -85,7 +85,7 @@ func buildComponentObjectRefs(components ...string) ([]object.ObjMetadata, error
var objRefs []object.ObjMetadata var objRefs []object.ObjMetadata
for _, deployment := range components { for _, deployment := range components {
objRefs = append(objRefs, object.ObjMetadata{ objRefs = append(objRefs, object.ObjMetadata{
Namespace: GetDesiredNamespace(kubeconfigArgs), Namespace: *kubeconfigArgs.Namespace,
Name: deployment, Name: deployment,
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
}) })

View File

@@ -1,3 +1,3 @@
► checking prerequisites ► checking prerequisites
✔ Kubernetes {{ .serverVersion }} >=1.25.0-0 ✔ Kubernetes {{ .serverVersion }} >=1.24.0-0
✔ prerequisites checks passed ✔ prerequisites checks passed

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: podinfo
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
spec:
interval: 5m0s
path: ./kustomize
force: true
prune: true
sourceRef:
kind: GitRepository
name: podinfo
targetNamespace: default

View File

@@ -333,7 +333,7 @@ func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error)
k := &kustomizev1.Kustomization{} k := &kustomizev1.Kustomization{}
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(data), len(data)) decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(data), len(data))
// check for kustomization in yaml with the same name and namespace // check for kustomization in yaml with the same name and namespace
for { for !(k.Name == b.name && (k.Namespace == b.namespace || k.Namespace == "")) {
err = decoder.Decode(k) err = decoder.Decode(k)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
@@ -343,13 +343,6 @@ func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error)
return nil, fmt.Errorf("failed to unmarshall kustomization file %s: %w", b.kustomizationFile, err) return nil, fmt.Errorf("failed to unmarshall kustomization file %s: %w", b.kustomizationFile, err)
} }
} }
if strings.HasPrefix(k.APIVersion, kustomizev1.GroupVersion.Group+"/") &&
k.Kind == kustomizev1.KustomizationKind &&
k.Name == b.name &&
(k.Namespace == b.namespace || k.Namespace == "") {
break
}
} }
return k, nil return k, nil
} }

View File

@@ -189,12 +189,6 @@ func Test_unMarshallKustomization(t *testing.T) {
wantErr: true, wantErr: true,
errString: "failed find kustomization with name", errString: "failed find kustomization with name",
}, },
{
name: "yaml containing other resource with same name as kustomization",
localKsFile: "testdata/local-kustomization/invalid-resource.yaml",
wantErr: true,
errString: "failed find kustomization with name",
},
} }
b := &Builder{ b := &Builder{
@@ -330,10 +324,7 @@ func Test_ResolveKustomization(t *testing.T) {
}, },
} }
b := &Builder{ b := &Builder{}
name: "podinfo",
namespace: "flux-system",
}
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
b.kustomizationFile = tt.localKsFile b.kustomizationFile = tt.localKsFile

View File

@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: podinfo

View File

@@ -0,0 +1,209 @@
# RFC-0006 Passwordless authentication for Git repositories
**Status:** provisional
**Creation date:** 2023-31-07
## Summary
Flux should provide a mechanism to authenticate against Git repositories without
the use of passwords. This RFC proposes the use of alternative authentication
methods like OIDC, OAuth2 and IAM to access Git repositories hosted on specific
Git SaaS platforms and cloud providers.
## Motivation
At the moment, Flux supports HTTP basic and bearer authentication. Users are
required to create a Secret containing the username and the password/bearer
token, which is then referred to in the GitRepository using `.spec.secretRef`.
While this works fine, it has a couple of drawbacks:
* Scalability: Each new GitRepository potentially warrants another credentials
pair, which doesn't scale well in big organizations with hundreds of
repositories with different owners, increasing the risk of mismanagement and
leaks.
* Identity: A username is associated with an actual human. But often, the
repository belongs to a team of 2 or more people. This leads to a problem where
teams have to decide whose credentials should Flux use for authentication.
These problems exist not due to flaws in Flux, but because of the inherent
nature of password based authentication.
With support for OIDC, OAuth2 and IAM based authentication, we can eliminate
these problems:
* Scalability: Since OIDC is fully handled by the cloud provider, it eliminates
any user involvement in managing credentials. For OAuth2 and IAM, users do need
to provide certain information like the ID of the resource, private key, etc.
but these are still a better alternative to passwords since the same resource
can be reused by multiple teams with different members.
* Identity: Since all the above authentication methods are associated with a
virtual resource independent of a user, it solves the problem of a single person
being tied to automation that several people are involved in.
### Goals
* Integrate with major cloud providers' OIDC and IAM offerings to provide a
seamless way of Git repository authentication.
* Integrate with major Git SaaS providers to support their app based OAuth2
mechanism.
### Non-Goals
* Replace the existing basic and bearer authentication API.
## Proposal
A new string field `.spec.provider` shall be added to the GitRepository API.
The field will be an enum with the following variants:
* `azure`
* `github`
* `gcp`
> AWS CodeCommit is not supported as it does not support authentication via IAM
Roles without the use of https://github.com/aws/git-remote-codecommit.
By default, it will be blank, which indicates that the user wants to
authenticate via HTTP basic/bearer auth or SSH.
### Azure
Git repositories hosted on Azure Devops can be accessed by Flux using OIDC if
the cluster running Flux is hosted on AKS with [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops)
enabled. The managed identity associated with the cluster must have sufficient
permissions to be able to access Azure Devops resources. This enables Flux to
access the Git repository without the need for any credentials.
```yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: azure-devops
spec:
interval: 1m
url: https://dev.azure.com/<org>/<project>/_git/<repository>
ref:
branch: master
# notice the lack of secretRef
provider: azure
```
### GCP
Git repositories hosted on Google Cloud Source Repositories can be accessed by
Flux via a [GCP Service Account](https://cloud.google.com/iam/docs/service-account-overview).
The Service Account must have sufficient permissions to be able to access Google
Cloud Source Repositories and its credentials should be specified in the secret
referred to in `.spec.secretRef`.
```yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: gcp-repo
spec:
interval: 1m
url: https://source.developers.google.com/p/<project>/r/<repository>
ref:
branch: master
provider: gcp
secretRef:
name: gcp-sa
---
kind: Secret
metadata:
name: gcp-sa
stringData:
gcpServiceAccount: |
{
"type": "service_account",
"project_id": "my-google-project",
"private_key_id": "REDACTED",
"private_key": "-----BEGIN PRIVATE KEY-----\nREDACTED\n-----END PRIVATE KEY-----\n",
"client_email": "<service-account-id>@my-google-project.iam.gserviceaccount.com",
"client_id": "REDACTED",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/<service-account-id>%40my-google-project.iam.gserviceaccount.com"
}
```
### GitHub
Git repositories hosted on GitHub can be accessed via [GitHub Apps](https://docs.github.com/en/apps/overview).
This allows users to create a single resource from which they can access all
their GitHub repositories. The app must have sufficient permissions to be able
to access repositories. The app's ID, private key and installation ID should
be mentioned in the Secret referred to by `.spec.secretRef`. GitHub Enterprise
users will also need to mention their GitHub API URL in the Secret.
```yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: github-repo
spec:
interval: 1m
url: https://github.com/<org>/<repository>
ref:
branch: master
provider: github
secretRef:
name: github-app
---
kind: Secret
metadata:
name: gcp-sa
stringData:
githubAppID: <app-id>
githubInstallationID: <installation-id>
githubPrivateKey: |
<PEM-private-key>
githubApiURl: <github-enterprise-api-url> #optional, required only for GitHub Enterprise users
```
## Design Details
### Azure
If `.spec.provider` is set to `azure`, Flux controllers will reach out to
[Azure IMDS (Instance Metadata Service)](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-go)
to get an access token. This [access token will be then used as a bearer token](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops#q-can-i-use-a-service-principal-to-do-git-operations-like-clone-a-repo)
to perform HTTP bearer authentication.
### GCP
If `.spec.provider` is set to `gcp`, Flux controllers will get the Service
Account credentials from the specified Secret and use
[`google.CredentialsFromJSON`](https://pkg.go.dev/golang.org/x/oauth2/google#CredentialsFromJSON)
to fetch the access token. This access token will be then used as the password
and the `client_email` as the username to perform HTTP basic authentication.
### GitHub
If `.spec.provider` is set to `github`, Flux controllers will get the app
details from the specified Secret and use it to [generate an app installation
token](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app).
This token is then used as the password and [`x-access-token` as the username](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app#choosing-permissions-for-git-access)
to perform HTTP basic authentication.
### Token Caching and Refreshing
To avoid calling the upstream API for a token during every reconciliation, Flux
controllers shall cache the token after fetching it. Since GitHub tokens
self-expire, the cache shall automatically evict the token after it has expired,
triggering a fetch of a fresh token.
For GCP, the [`TokenSource`](https://pkg.go.dev/golang.org/x/oauth2@v0.10.0#TokenSource)
object will be cached, since it automatically handles refreshing an expired
token and always returns a valid token. Since a `TokenSource` never expires, it
need not be evicted from the cache.
While Azure's managed identities subsystem caches the token, it is
[recommended for the consumer application to implement their own caching](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#token-caching)
as well.
The caches for all three providers are separate, i.e. there shall exist a
dedicated cache for each provider.
Since the proposed authentication methods for GitHub and GCP involve some form
of credentials stored in a Kubernetes Secret, the cache key can be the Secret's
`<namespace/name>`. Since authentication for Azure is configured directly via
the source-controller Deployment, the token can just be stored in a global
variable, which is refreshed whenever the token expires.