Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/argocd.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ kubectl get app -n argocd # everything, one line each

`app` is the short name for `applications.argoproj.io`. Add `-w` to watch.

> **This page assumes infra-level access.** A project developer cannot list
> Applications and must name their own — see
> [onboarding.md §D](onboarding.md#d-see-your-own-argocd-sync-status-optional).

**Only what needs attention** — the single most useful command:

```bash
Expand Down Expand Up @@ -199,6 +203,7 @@ ApplicationSet or an app-of-apps — the parent overwrites it. Edit the file in
| A common service | `k8s/argocd/infra-apps/<svc>.yaml` + its values under `k8s/infra-manifest/` | `infra-root` |
| An AppProject | `k8s/argocd/projects/*.yaml` | `argocd-projects` (wave -1) |
| A project's namespace / RBAC / DB | `k8s/projects/<project>/infra/` | `project-infra` ApplicationSet |
| Letting a project read its own sync status | `k8s/projects/<project>/infra/argocd-status-rbac.yaml` | `project-infra` ApplicationSet |
| A project's own AppProject (its repo + namespaces + what it may deploy) | `k8s/argocd/projects/<project>.yaml` | `argocd-projects` (wave -1) |
| Registering a project's own GitOps repo | `k8s/projects/<project>/gitops.yaml` | `project-gitops` ApplicationSet |
| A project's workload itself | **the project's own repo** — not this one | the generated `<project>-<env>` Application |
Expand Down
52 changes: 52 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ say why rather than deleting it.
| [11](#11-alerts-go-to-slack-and-info-level-is-dropped) | Alerts go to Slack, and info-level is dropped | current |
| [12](#12-gitops-is-argocd-not-flux) | GitOps is ArgoCD, not Flux | settled |
| [13](#13-the-default-appproject-is-emptied) | The `default` AppProject is emptied | current |
| [14](#14-projects-read-their-own-argocd-status-via-kubernetes-rbac-not-an-argocd-ui) | Projects read their own ArgoCD status via RBAC, not an ArgoCD UI | current |

---

Expand Down Expand Up @@ -519,3 +520,54 @@ permissions. That is the intended behaviour: project assignment becomes
deliberate. An Application that suddenly cannot sync after this lands is telling
you it never named a project.


## 14. Projects read their own ArgoCD status via Kubernetes RBAC, not an ArgoCD UI

**Current.** A project that wants to see whether ArgoCD is syncing its manifests
gets a `Role` in the `argocd` namespace scoped with `resourceNames` to its own
`Application` objects, bound to its GitHub team. There is no ArgoCD web UI, and
`argocd-server` is not exposed. The template is
`k8s/projects/_template/infra/argocd-status-rbac.yaml.example`; it is opt-in, and
a project without it sees nothing in `argocd` at all.

**Why this and not the UI.** Projects could already see the *effects* of a sync —
Deployments, Pods and Events in their own namespaces, through Headlamp. What they
could not see is a sync that is **failing**, which is the case that matters: a
stalled sync looks exactly like "nothing has happened yet". Verified by
impersonation before this landed: a project developer got `no` for `get`, `list`
and `watch` on `applications.argoproj.io`.

Exposing the ArgoCD UI behind Dex would also answer it, and `install.md` had
always left that door open. It was not taken because it costs a new public
endpoint, a second authorisation model (`argocd-rbac-cm`, currently empty, with
an unrestricted break-glass `admin` account) and per-project RBAC lines that must
be generated during onboarding or silently drift. The RBAC route reuses the
identity and the tool projects already have.

**The limitation, stated plainly: `kubectl get app -n argocd` is Forbidden.**
Kubernetes RBAC cannot filter a collection, so `resourceNames` does not restrict
`list` — it only permits `get` on named objects. A developer must therefore name
the Application:

```
kubectl get app -n argocd project-infra-<project> \
-o custom-columns='SYNC:.status.sync.status,HEALTH:.status.health.status'
```

The alternative was granting unscoped `list`, which would expose every project's
Application to every project. A label selector does not help — the filtering is
client-side and the request is refused before it. The same applies in Headlamp,
which lists resources: the Application will not appear in a list view.

**Consequence.** `project-infra` gains `Role` in its
`namespaceResourceWhitelist`, alongside the `RoleBinding` it already had. That
widens the cluster's most privileged AppProject by one kind, which is why the
Role is written per project with explicit `resourceNames` rather than as a shared
ClusterRole: a Role grants no more than the verbs written in it, and only inside
its own namespace. Verbs are `get` and `watch` only — not `patch`, not `delete`,
so a project cannot trigger or abort its own sync. Confirmed by impersonation,
including that another project's team is refused.

**Revisit if** projects ask for the diff view or a self-service sync button.
Those are real arguments for the UI, and the per-project `resourceNames` work
done here is not wasted if it is built.
20 changes: 15 additions & 5 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -1372,19 +1372,29 @@ smaller attack surface, matches the "everything in Git" model.
kubectl get app -n argocd # state of everything
kubectl annotate app -n argocd <app> \
argocd.argoproj.io/refresh=hard --overwrite # re-read Git now
# sync — syncOptions must be carried over explicitly, see argocd.md
OPTS=$(kubectl get app -n argocd <app> -o jsonpath='{.spec.syncPolicy.syncOptions}')
kubectl patch app -n argocd <app> --type merge \
-p '{"operation":{"initiatedBy":{"username":"'"$USER"'"},"sync":{}}}' # sync
-p '{"operation":{"initiatedBy":{"username":"'"$USER"'"},"sync":{"syncOptions":'"${OPTS:-[]}"'}}}'
```
- **Debug** a stuck sync via a temporary
`kubectl -n argocd port-forward svc/argocd-server 8080:443`, logging in with the
break-glass admin (`kubectl -n argocd get secret argocd-initial-admin-secret
-o jsonpath='{.data.password}' | base64 -d`). Not for daily use — the kubectl
path above needs no shared credential.

If a shared dashboard is ever wanted, expose `argocd-server` via Traefik + GitHub
OAuth via ArgoCD's bundled Dex (Scouterna org, teams → RBAC). Not done here by
choice. Note that until it is, `argocd-rbac-cm` is empty and the break-glass
`admin` is unrestricted — one more reason it is not for daily use.
**Projects see their own sync status without any of this** — a Role in `argocd`
scoped to their own Applications, described in
[onboarding.md](onboarding.md#d-see-your-own-argocd-sync-status-optional) and
[decisions.md entry 14](decisions.md#14-projects-read-their-own-argocd-status-via-kubernetes-rbac-not-an-argocd-ui).

If a shared dashboard is ever wanted anyway, expose `argocd-server` via Traefik
and point ArgoCD's `oidc.config` at the **standalone Dex this cluster already
runs** (§2b) — not ArgoCD's bundled Dex, which would be a second identity
provider for the same GitHub org. Note that until then, `argocd-rbac-cm` is empty
and the break-glass `admin` is unrestricted: exposing the UI without first
setting a default policy of `role:''` would be a real escalation, not a small
one. One more reason it is not for daily use.

> **A sync overwrites hand edits.** An app left on manual sync (no `automated:`)
> lets a project change the release in the cluster and keeps that drift — but the
Expand Down
45 changes: 44 additions & 1 deletion docs/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ is excluded.
grep -rlZ PROJECT "k8s/projects/$PROJECT/" | xargs -0 sed -i "s/PROJECT/$PROJECT/g"
```

**Check it before moving on** — the file list should be 6 files, all under
**Check it before moving on** — the file list should be 7 files, all under
`k8s/projects/$PROJECT/`, and no `PROJECT` may remain:

```bash
Expand Down Expand Up @@ -435,6 +435,49 @@ which does not match this table exactly:
If you find yourself able to do something on this list, treat it as a gap to
report rather than a shortcut.

## D. See your own ArgoCD sync status (optional)

Your namespaces show you what **is** running. They do not show you whether ArgoCD
is *failing to apply* something — and a stalled sync looks exactly like "my change
has not arrived yet". This section closes that gap.

It is opt-in and needs the infra team once. Ask them to copy
`k8s/projects/_template/infra/argocd-status-rbac.yaml.example` into your project's
`infra/` directory as `argocd-status-rbac.yaml`, with your project name, your
GitHub team, and the Applications you should see. After it syncs:

```bash
kubectl get app -n argocd project-infra-<project> \
-o custom-columns='SYNC:.status.sync.status,HEALTH:.status.health.status'
```

`SYNC: Synced` means the cluster matches Git. `OutOfSync` means it does not —
either a sync is in flight, or one failed. For the reason:

```bash
kubectl get app -n argocd project-infra-<project> \
-o jsonpath='{.status.operationState.phase}{" "}{.status.operationState.message}{"\n"}'
```

**Which Applications exist depends on your route** (section C):

| Application | When |
|---|---|
| `project-infra-<project>` | always — your namespaces, developer access, database |
| `<project>-dev`, `<project>-prod` | only if you run your own GitOps repo (C2) |

Each one must be listed by name in the Role, so tell infra which you want.

**Two things this does not do**, both deliberate ([decisions.md entry
14](decisions.md#14-projects-read-their-own-argocd-status-via-kubernetes-rbac-not-an-argocd-ui)):

- **`kubectl get app -n argocd` without a name is Forbidden**, and the
Applications do not appear in Headlamp's list view. Kubernetes RBAC cannot
filter a list, so access is granted per named object; permitting the list would
expose every project's Application to every project. Name yours and it works.
- **Read-only.** You can watch a sync; you cannot start or stop one. If you need
to force a sync, ask infra — [argocd.md](argocd.md) has the procedure.

## Secrets

Three paths, pick by how the secret should be owned:
Expand Down
2 changes: 1 addition & 1 deletion k8s/argocd/projects-root/projectset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ spec:
# namespace.yaml covers a single-namespace project; namespace-*.yaml
# covers dev/prod/staging etc. A file that matches nothing here is
# ignored SILENTLY — extend this list when adding a manifest kind.
include: '{namespace.yaml,namespace-*.yaml,developer-rbac.yaml,database.yaml,sealedsecret-*.yaml}'
include: '{namespace.yaml,namespace-*.yaml,developer-rbac.yaml,argocd-status-rbac.yaml,database.yaml,sealedsecret-*.yaml}'
destination:
server: https://kubernetes.default.svc
syncPolicy:
Expand Down
5 changes: 5 additions & 0 deletions k8s/argocd/projects/project-infra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ spec:
kind: ServiceAccount
- group: 'rbac.authorization.k8s.io'
kind: RoleBinding
# Role is needed only for the per-project argocd-status Role, which must
# carry resourceNames and so cannot be a shared ClusterRole. A Role grants
# no more than the verbs written in it, and only within its own namespace.
- group: 'rbac.authorization.k8s.io'
kind: Role
- group: 'postgresql.cnpg.io'
kind: '*'
- group: 'external-secrets.io'
Expand Down
46 changes: 46 additions & 0 deletions k8s/projects/_template/infra/argocd-status-rbac.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Lets this project READ the sync status of its OWN ArgoCD Application(s).
# Copy to argocd-status-rbac.yaml (drop the .example) to activate it.
#
# Application objects live in the `argocd` namespace, which projects otherwise
# have no access to, so without this a project cannot tell whether ArgoCD is
# failing to sync its manifests — a stalled sync looks exactly like "nothing has
# happened yet". Read-only: no sync button, no diff view. For those, see
# docs/argocd.md.
#
# ⚠️ Applications must be listed by name in resourceNames, and the names differ
# per route (see docs/onboarding.md section D):
# project-infra-PROJECT — always exists (namespace, RBAC, database)
# PROJECT-dev / -prod — only if this project runs its OWN GitOps repo
# An Application not named here stays invisible. Adding one later means editing
# this file, so keep it in step with gitops.yaml.
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: PROJECT-argocd-status
namespace: argocd
rules:
- apiGroups: ["argoproj.io"]
resources: ["applications"]
verbs: ["get", "watch"]
resourceNames:
- project-infra-PROJECT
# Uncomment the pair below if this project has a gitops.yaml.
# - PROJECT-dev
# - PROJECT-prod
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: PROJECT-argocd-status
namespace: argocd
subjects:
# Same subject as developer-rbac.yaml — the GitHub team display name,
# verbatim, and it fails silently if wrong.
- kind: Group
name: "aks:jwt:Scouterna:TEAM"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: PROJECT-argocd-status
apiGroup: rbac.authorization.k8s.io