mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-03-26 18:38:48 +01:00
Compare commits
3 Commits
todaywasaw
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e972bfca78 | ||
|
|
1b405ce2b5 | ||
|
|
45b926d796 |
@@ -1,21 +1,21 @@
|
||||
# Keycloak
|
||||
Keycloak and Argo CD integration can be configured in two ways with Client authentication and with PKCE.
|
||||
Keycloak and ArgoCD integration can be configured in two ways with Client authentication and with PKCE.
|
||||
|
||||
If you need to authenticate with __argo-cd command line__, you must choose PKCE way.
|
||||
|
||||
* [Keycloak and Argo CD with Client authentication](#keycloak-and-argocd-with-client-authentication)
|
||||
* [Keycloak and Argo CD with PKCE](#keycloak-and-argocd-with-pkce)
|
||||
* [Keycloak and ArgoCD with Client authentication](#keycloak-and-argocd-with-client-authentication)
|
||||
* [Keycloak and ArgoCD with PKCE](#keycloak-and-argocd-with-pkce)
|
||||
|
||||
## Keycloak and Argo CD with Client authentication
|
||||
## Keycloak and ArgoCD with Client authentication
|
||||
|
||||
These instructions will take you through the entire process of getting your Argo CD application to authenticate with Keycloak.
|
||||
These instructions will take you through the entire process of getting your ArgoCD application authenticating with Keycloak.
|
||||
|
||||
Start by creating a client within Keycloak and configure Argo CD to use Keycloak for authentication, using groups set in Keycloak
|
||||
You will create a client within Keycloak and configure ArgoCD to use Keycloak for authentication, using groups set in Keycloak
|
||||
to determine privileges in Argo.
|
||||
|
||||
### Creating a new client in Keycloak
|
||||
|
||||
First, setup a new client.
|
||||
First we need to setup a new client.
|
||||
|
||||
Start by logging into your keycloak server, select the realm you want to use (`master` by default)
|
||||
and then go to __Clients__ and click the __Create client__ button at the top.
|
||||
@@ -37,11 +37,11 @@ but it's not recommended in production).
|
||||
|
||||
Make sure to click __Save__.
|
||||
|
||||
There should be a tab called __Credentials__. You can copy the Client Secret that we'll use in our Argo CD configuration.
|
||||
There should be a tab called __Credentials__. You can copy the Client Secret that we'll use in our ArgoCD configuration.
|
||||
|
||||

|
||||
|
||||
### Configuring Argo CD OIDC
|
||||
### Configuring ArgoCD OIDC
|
||||
|
||||
Let's start by storing the client secret you generated earlier in the argocd secret _argocd-secret_.
|
||||
|
||||
@@ -68,7 +68,7 @@ data:
|
||||
clientID: argocd
|
||||
clientSecret: $oidc.keycloak.clientSecret
|
||||
refreshTokenThreshold: 2m
|
||||
requestedScopes: ["openid", "profile", "email", "groups", "offline_access"]
|
||||
requestedScopes: ["openid", "profile", "email", "groups"]
|
||||
```
|
||||
|
||||
Make sure that:
|
||||
@@ -80,18 +80,18 @@ Make sure that:
|
||||
- __requestedScopes__ contains the _groups_ claim if you didn't add it to the Default scopes
|
||||
- __refreshTokenThreshold__ is less than the client token lifetime. If this setting is not less than the token lifetime, a new token will be obtained for every request. Keycloak sets the client token lifetime to 5 minutes by default.
|
||||
|
||||
## Keycloak and Argo CD with PKCE
|
||||
## Keycloak and ArgoCD with PKCE
|
||||
|
||||
These instructions will take you through the entire process of getting your Argo CD application authenticating with Keycloak.
|
||||
These instructions will take you through the entire process of getting your ArgoCD application authenticating with Keycloak.
|
||||
|
||||
You will create a client within Keycloak and configure Argo CD to use Keycloak for authentication, using groups set in Keycloak
|
||||
You will create a client within Keycloak and configure ArgoCD to use Keycloak for authentication, using groups set in Keycloak
|
||||
to determine privileges in Argo.
|
||||
|
||||
You will also be able to authenticate using argo-cd command line.
|
||||
|
||||
### Creating a new client in Keycloak
|
||||
|
||||
First, setup a new client.
|
||||
First we need to setup a new client.
|
||||
|
||||
Start by logging into your keycloak server, select the realm you want to use (`master` by default)
|
||||
and then go to __Clients__ and click the __Create client__ button at the top.
|
||||
@@ -119,7 +119,7 @@ Now go to a tab called __Advanced__, look for parameter named __Proof Key for Co
|
||||

|
||||
Make sure to click __Save__.
|
||||
|
||||
### Configuring Argo CD OIDC
|
||||
### Configuring ArgoCD OIDC
|
||||
Now we can configure the config map and add the oidc configuration to enable our keycloak authentication.
|
||||
You can use `$ kubectl edit configmap argocd-cm`.
|
||||
|
||||
@@ -138,7 +138,7 @@ data:
|
||||
clientID: argocd
|
||||
enablePKCEAuthentication: true
|
||||
refreshTokenThreshold: 2m
|
||||
requestedScopes: ["openid", "profile", "email", "groups", "offline_access"]
|
||||
requestedScopes: ["openid", "profile", "email", "groups"]
|
||||
```
|
||||
|
||||
Make sure that:
|
||||
@@ -146,13 +146,13 @@ Make sure that:
|
||||
- __issuer__ ends with the correct realm (in this example _master_)
|
||||
- __issuer__ on Keycloak releases older than version 17 the URL must include /auth (in this example /auth/realms/master)
|
||||
- __clientID__ is set to the Client ID you configured in Keycloak
|
||||
- __enablePKCEAuthentication__ must be set to true to enable correct Argo CD behaviour with PKCE
|
||||
- __enablePKCEAuthentication__ must be set to true to enable correct ArgoCD behaviour with PKCE
|
||||
- __requestedScopes__ contains the _groups_ claim if you didn't add it to the Default scopes
|
||||
- __refreshTokenThreshold__ is less than the client token lifetime. If this setting is not less than the token lifetime, a new token will be obtained for every request. Keycloak sets the client token lifetime to 5 minutes by default.
|
||||
|
||||
## Configuring the groups claim
|
||||
|
||||
In order for Argo CD to provide the groups the user is in we need to configure a groups claim that can be included in the authentication token.
|
||||
In order for ArgoCD to provide the groups the user is in we need to configure a groups claim that can be included in the authentication token.
|
||||
|
||||
To do this we'll start by creating a new __Client Scope__ called _groups_.
|
||||
|
||||
@@ -174,7 +174,7 @@ Go back to the client we've created earlier and go to the Tab "Client Scopes".
|
||||
Click on "Add client scope", choose the _groups_ scope and add it either to the __Default__ or to the __Optional__ Client Scope.
|
||||
|
||||
If you put it in the Optional
|
||||
category you will need to make sure that Argo CD requests the scope in its OIDC configuration.
|
||||
category you will need to make sure that ArgoCD requests the scope in its OIDC configuration.
|
||||
Since we will always want group information, I recommend
|
||||
using the Default category.
|
||||
|
||||
@@ -184,7 +184,7 @@ Create a group called _ArgoCDAdmins_ and have your current user join the group.
|
||||
|
||||

|
||||
|
||||
## Configuring Argo CD Policy
|
||||
## Configuring ArgoCD Policy
|
||||
|
||||
Now that we have an authentication that provides groups we want to apply a policy to these groups.
|
||||
We can modify the _argocd-rbac-cm_ ConfigMap using `$ kubectl edit configmap argocd-rbac-cm`.
|
||||
@@ -205,7 +205,7 @@ In this example we give the role _role:admin_ to all users in the group _ArgoCDA
|
||||
|
||||
You can now login using our new Keycloak OIDC authentication:
|
||||
|
||||

|
||||

|
||||
|
||||
If you have used PKCE method, you can also authenticate using command line:
|
||||
```bash
|
||||
@@ -219,7 +219,7 @@ Once done, you should see
|
||||

|
||||
|
||||
## Troubleshoot
|
||||
If Argo CD auth returns 401 or when the login attempt leads to the loop, then restart the argocd-server pod.
|
||||
If ArgoCD auth returns 401 or when the login attempt leads to the loop, then restart the argocd-server pod.
|
||||
```
|
||||
kubectl rollout restart deployment argocd-server -n argocd
|
||||
```
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import {AppsListPreferences, AppSetsListPreferences, services} from '../../../shared/services';
|
||||
import {Filter, FiltersGroup} from '../filter/filter';
|
||||
import {createMetadataSelector} from '../selectors';
|
||||
import {ComparisonStatusIcon, getAppSetHealthStatus, HealthStatusIcon, getOperationStateTitle} from '../utils';
|
||||
import {ComparisonStatusIcon, getAppAllSources, getAppSetHealthStatus, HealthStatusIcon, getOperationStateTitle} from '../utils';
|
||||
import {formatClusterQueryParam} from '../../../shared/utils';
|
||||
import {COLORS} from '../../../shared/components/colors';
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface FilterResult {
|
||||
health: boolean;
|
||||
clusters: boolean;
|
||||
namespaces: boolean;
|
||||
targetRevision: boolean;
|
||||
operation: boolean;
|
||||
annotations: boolean;
|
||||
favourite: boolean;
|
||||
@@ -59,31 +60,41 @@ export function getAppFilterResults(applications: Application[], pref: AppsListP
|
||||
const labelSelector = createMetadataSelector(pref.labelsFilter || []);
|
||||
const annotationSelector = createMetadataSelector(pref.annotationsFilter || []);
|
||||
|
||||
return applications.map(app => ({
|
||||
...app,
|
||||
filterResult: {
|
||||
sync: pref.syncFilter.length === 0 || pref.syncFilter.includes(app.status.sync.status),
|
||||
autosync: pref.autoSyncFilter.length === 0 || pref.autoSyncFilter.includes(getAutoSyncStatus(app.spec.syncPolicy)),
|
||||
health: pref.healthFilter.length === 0 || pref.healthFilter.includes(app.status.health.status),
|
||||
namespaces: pref.namespacesFilter.length === 0 || pref.namespacesFilter.some(ns => app.spec.destination.namespace && minimatch(app.spec.destination.namespace, ns)),
|
||||
favourite: !pref.showFavorites || (pref.favoritesAppList && pref.favoritesAppList.includes(app.metadata.name)),
|
||||
clusters:
|
||||
pref.clustersFilter.length === 0 ||
|
||||
pref.clustersFilter.some(filterString => {
|
||||
const match = filterString.match('^(.*) [(](http.*)[)]$');
|
||||
if (match?.length === 3) {
|
||||
const [, name, url] = match;
|
||||
return url === app.spec.destination.server || name === app.spec.destination.name;
|
||||
} else {
|
||||
const inputMatch = filterString.match('^http.*$');
|
||||
return (inputMatch && inputMatch[0] === app.spec.destination.server) || (app.spec.destination.name && minimatch(app.spec.destination.name, filterString));
|
||||
}
|
||||
}),
|
||||
labels: pref.labelsFilter.length === 0 || labelSelector(app.metadata.labels),
|
||||
annotations: pref.annotationsFilter.length === 0 || annotationSelector(app.metadata.annotations),
|
||||
operation: pref.operationFilter.length === 0 || pref.operationFilter.includes(getOperationStateTitle(app))
|
||||
}
|
||||
}));
|
||||
return applications.map(app => {
|
||||
const targetRevisions = getAppAllSources(app)
|
||||
.map(source => source.targetRevision)
|
||||
.filter((item): item is string => !!item);
|
||||
|
||||
return {
|
||||
...app,
|
||||
filterResult: {
|
||||
sync: pref.syncFilter.length === 0 || pref.syncFilter.includes(app.status.sync.status),
|
||||
autosync: pref.autoSyncFilter.length === 0 || pref.autoSyncFilter.includes(getAutoSyncStatus(app.spec.syncPolicy)),
|
||||
health: pref.healthFilter.length === 0 || pref.healthFilter.includes(app.status.health.status),
|
||||
namespaces: pref.namespacesFilter.length === 0 || pref.namespacesFilter.some(ns => app.spec.destination.namespace && minimatch(app.spec.destination.namespace, ns)),
|
||||
favourite: !pref.showFavorites || (pref.favoritesAppList && pref.favoritesAppList.includes(app.metadata.name)),
|
||||
clusters:
|
||||
pref.clustersFilter.length === 0 ||
|
||||
pref.clustersFilter.some(filterString => {
|
||||
const match = filterString.match('^(.*) [(](http.*)[)]$');
|
||||
if (match?.length === 3) {
|
||||
const [, name, url] = match;
|
||||
return url === app.spec.destination.server || name === app.spec.destination.name;
|
||||
} else {
|
||||
const inputMatch = filterString.match('^http.*$');
|
||||
return (
|
||||
(inputMatch && inputMatch[0] === app.spec.destination.server) || (app.spec.destination.name && minimatch(app.spec.destination.name, filterString))
|
||||
);
|
||||
}
|
||||
}),
|
||||
targetRevision:
|
||||
pref.targetRevisionFilter.length === 0 || pref.targetRevisionFilter.some(filter => targetRevisions.some(targetRevision => minimatch(targetRevision, filter))),
|
||||
labels: pref.labelsFilter.length === 0 || labelSelector(app.metadata.labels),
|
||||
annotations: pref.annotationsFilter.length === 0 || annotationSelector(app.metadata.annotations),
|
||||
operation: pref.operationFilter.length === 0 || pref.operationFilter.includes(getOperationStateTitle(app))
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppSetFilterResults(appSets: ApplicationSet[], pref: AppSetsListPreferences): ApplicationSetFilteredApp[] {
|
||||
@@ -361,6 +372,26 @@ const NamespaceFilter = React.memo((props: AppFilterProps) => {
|
||||
);
|
||||
});
|
||||
|
||||
const TargetRevisionFilter = (props: AppFilterProps) => {
|
||||
const targetRevisionOptions = React.useMemo(
|
||||
() =>
|
||||
optionsFrom(
|
||||
Array.from(new Set(props.apps.flatMap(app => getAppAllSources(app).map(source => source.targetRevision)).filter((item): item is string => !!item))),
|
||||
props.pref.targetRevisionFilter
|
||||
),
|
||||
[props.apps, props.pref.targetRevisionFilter]
|
||||
);
|
||||
return (
|
||||
<Filter
|
||||
label='TARGET REVISION'
|
||||
selected={props.pref.targetRevisionFilter}
|
||||
setSelected={s => props.onChange({...props.pref, targetRevisionFilter: s})}
|
||||
field={true}
|
||||
options={targetRevisionOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const FavoriteFilter = (props: {value: boolean; onChange: (showFavorites: boolean) => void}) => {
|
||||
const onChange = (val: boolean) => {
|
||||
props.onChange(val);
|
||||
@@ -468,9 +499,11 @@ export const ApplicationsFilter = (props: AppFilterProps) => {
|
||||
...(props.pref.healthFilter || []),
|
||||
...(props.pref.operationFilter || []),
|
||||
...(props.pref.labelsFilter || []),
|
||||
...(props.pref.annotationsFilter || []),
|
||||
...(props.pref.projectsFilter || []),
|
||||
...(props.pref.clustersFilter || []),
|
||||
...(props.pref.namespacesFilter || []),
|
||||
...(props.pref.targetRevisionFilter || []),
|
||||
...(props.pref.autoSyncFilter || []),
|
||||
...(props.pref.showFavorites ? ['favorites'] : [])
|
||||
];
|
||||
@@ -492,6 +525,7 @@ export const ApplicationsFilter = (props: AppFilterProps) => {
|
||||
<ProjectFilter {...props} />
|
||||
<ClusterFilter {...props} />
|
||||
<NamespaceFilter {...props} />
|
||||
<TargetRevisionFilter {...props} />
|
||||
<AutoSyncFilter {...props} collapsed={true} />
|
||||
</FiltersGroup>
|
||||
);
|
||||
|
||||
@@ -160,6 +160,13 @@ const ViewPref = ({children}: {children: (pref: AppsListPreferences & {page: num
|
||||
.split(',')
|
||||
.filter(item => !!item);
|
||||
}
|
||||
if (params.get('targetRevision') != null) {
|
||||
viewPref.targetRevisionFilter = params
|
||||
.get('targetRevision')
|
||||
.split(',')
|
||||
.map(decodeURIComponent)
|
||||
.filter(item => !!item);
|
||||
}
|
||||
if (params.get('cluster') != null) {
|
||||
viewPref.clustersFilter = params
|
||||
.get('cluster')
|
||||
@@ -473,6 +480,7 @@ export const ApplicationsList = (props: RouteComponentProps<any> & {objectListKi
|
||||
autoSync: newPref.autoSyncFilter.join(','),
|
||||
health: newPref.healthFilter.join(','),
|
||||
namespace: newPref.namespacesFilter.join(','),
|
||||
targetRevision: newPref.targetRevisionFilter.map(encodeURIComponent).join(','),
|
||||
cluster: newPref.clustersFilter.join(','),
|
||||
labels: newPref.labelsFilter.map(encodeURIComponent).join(','),
|
||||
annotations: newPref.annotationsFilter.map(encodeURIComponent).join(','),
|
||||
|
||||
@@ -1479,6 +1479,34 @@ export function getAppDrySource(app?: appModels.Application): appModels.Applicat
|
||||
return {repoURL, targetRevision, path};
|
||||
}
|
||||
|
||||
// getAppAllSources gets all app sources as an array. For single source apps, returns [source].
|
||||
// For multi-source apps, returns the sources array. For sourceHydrator apps, returns a single synthesized source.
|
||||
export function getAppAllSources(app?: appModels.Application): appModels.ApplicationSource[] {
|
||||
if (!app) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (app.spec.sourceHydrator) {
|
||||
return [
|
||||
{
|
||||
repoURL: app.spec.sourceHydrator.drySource.repoURL,
|
||||
targetRevision: app.spec.sourceHydrator.syncSource.targetBranch,
|
||||
path: app.spec.sourceHydrator.syncSource.path
|
||||
} as appModels.ApplicationSource
|
||||
];
|
||||
}
|
||||
|
||||
if (app.spec.sources && app.spec.sources.length > 0) {
|
||||
return app.spec.sources;
|
||||
}
|
||||
|
||||
if (app.spec.source) {
|
||||
return [app.spec.source];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// getAppDefaultSyncRevision gets the first app revisions from `status.sync.revisions` or, if that list is missing or empty, the `revision`
|
||||
// field.
|
||||
export function getAppDefaultSyncRevision(app?: appModels.Application) {
|
||||
|
||||
@@ -91,6 +91,7 @@ export class AppsListPreferences extends AbstractAppsListPreferences {
|
||||
|
||||
pref.clustersFilter = [];
|
||||
pref.namespacesFilter = [];
|
||||
pref.targetRevisionFilter = [];
|
||||
pref.projectsFilter = [];
|
||||
pref.syncFilter = [];
|
||||
pref.autoSyncFilter = [];
|
||||
@@ -102,6 +103,7 @@ export class AppsListPreferences extends AbstractAppsListPreferences {
|
||||
public autoSyncFilter: string[];
|
||||
public namespacesFilter: string[];
|
||||
public clustersFilter: string[];
|
||||
public targetRevisionFilter: string[];
|
||||
public operationFilter: string[];
|
||||
}
|
||||
|
||||
@@ -156,6 +158,7 @@ const DEFAULT_PREFERENCES: ViewPreferences = {
|
||||
annotationsFilter: new Array<string>(),
|
||||
projectsFilter: new Array<string>(),
|
||||
namespacesFilter: new Array<string>(),
|
||||
targetRevisionFilter: new Array<string>(),
|
||||
clustersFilter: new Array<string>(),
|
||||
syncFilter: new Array<string>(),
|
||||
autoSyncFilter: new Array<string>(),
|
||||
@@ -228,6 +231,7 @@ export class ViewPreferencesService {
|
||||
appList.annotationsFilter = appList.annotationsFilter || [];
|
||||
appList.projectsFilter = appList.projectsFilter || [];
|
||||
appList.namespacesFilter = appList.namespacesFilter || [];
|
||||
appList.targetRevisionFilter = appList.targetRevisionFilter || [];
|
||||
appList.clustersFilter = appList.clustersFilter || [];
|
||||
appList.syncFilter = appList.syncFilter || [];
|
||||
appList.autoSyncFilter = appList.autoSyncFilter || [];
|
||||
|
||||
@@ -9705,9 +9705,9 @@ yaml-ast-parser@0.0.43:
|
||||
integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
|
||||
|
||||
yaml@^1.10.0:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
version "1.10.3"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.3.tgz#76e407ed95c42684fb8e14641e5de62fe65bbcb3"
|
||||
integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==
|
||||
|
||||
yargs-parser@^20.2.2:
|
||||
version "20.2.9"
|
||||
|
||||
Reference in New Issue
Block a user