iac.htora.dev · security templates

Home/Templates/Point Argo CD at a repository

Point Argo CD at a repository

The first Application, which then manages every other application from Git.

Argo CD

Why bother

Once Argo CD is installed it does nothing until you tell it which repository to follow. The usual mistake is adding applications one at a time through the web interface, which puts the list of what should be running in the one place that is not version controlled. One Application that points at a folder of Applications fixes that on day one.

How you know it worked

Add a folder to the repository. It appears in Argo CD without anyone running a command.

Set it up

Pick your platform, then open the level you want. Each level is complete on its own. Read the comments in the files as you go. Anything with a real consequence is explained on the line where it happens.

Argo CD. An Application pointing at a folder of Applications

What you need first

  • Argo CD already installed in the cluster.
  • A repository Argo CD can read, with a protected branch you are happy to have deploy itself.

What it creates

  • One Application in the argocd namespace
  • An AppProject fencing which repositories, namespaces, and cluster-wide kinds are allowed (Standard)
  • An ApplicationSet that turns each folder under apps/ into an application (Standard)

The code

Quick startDraft1 file, 54 lines
One Application watching a folder, with prune and self-heal on.
outcomes/gitops-bootstrap/kubernetes/t0
bootstrap.yaml
# gitops-bootstrap / kubernetes / t0 "Quick start"
#
# One Application that watches a folder of Applications. Add a file to that
# folder in Git and it appears in the cluster. This is usually called the
# app-of-apps pattern.
#
# Change repoURL to your repository, then:
#   kubectl apply -f bootstrap.yaml
#
# Verify:
#   Add a new Application file to apps/ in the repository and push.
#   It shows up in Argo CD within about three minutes, with nobody running
#   a command.
#
# Expected repository layout:
#   apps/
#     monitoring.yaml      <- each of these is an Argo CD Application
#     logging.yaml
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: bootstrap
  namespace: argocd
  # Without this finalizer, deleting the bootstrap Application leaves every
  # application it created running and unmanaged.
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default

  source:
    repoURL: https://github.com/your-org/your-cluster-config.git   # change me
    # Pin to a branch you protect. Pointing at HEAD means any push to any
    # branch can change production.
    targetRevision: main
    path: apps
    directory:
      recurse: true

  destination:
    # This exact string means the cluster Argo CD is running in.
    server: https://kubernetes.default.svc
    namespace: argocd

  syncPolicy:
    automated:
      # Remove from the cluster anything that was deleted from Git.
      prune: true
      # Undo changes made directly on the cluster. This is the setting that turns
      # Argo CD from a deployment tool into a control.
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
StandardDraft3 files, 145 lines
Adds the AppProject fence, an ApplicationSet generated from the folder layout, sync retries, server-side apply, and a sync role separate from an override role.
outcomes/gitops-bootstrap/kubernetes/t1
applicationset.yaml
# One definition that creates an Application for every folder under apps/.
# Adding a folder to the repository adds an application. Deleting the folder
# removes it. Nothing is clicked.
---
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: platform-apps
  namespace: argocd
spec:
  # This only matters if the ApplicationSet itself is deleted. True means the
  # platform services keep running, unmanaged, until someone decides what to do.
  # False means deleting one object removes every platform service at once.
  syncPolicy:
    preserveResourcesOnDeletion: true

  generators:
    - git:
        repoURL: https://github.com/your-org/your-cluster-config.git
        revision: main
        directories:
          - path: apps/*
          # Folders starting with an underscore hold shared pieces that other
          # applications include, so they get no Application of their own.
          - path: apps/_*
            exclude: true

  template:
    metadata:
      name: "{{path.basename}}"
      finalizers:
        - resources-finalizer.argocd.argoproj.io
      annotations:
        # Only commits touching this application's own folder trigger a
        # refresh, so a busy repository does not make every app regenerate.
        argocd.argoproj.io/manifest-generate-paths: "."
    spec:
      project: platform
      source:
        repoURL: https://github.com/your-org/your-cluster-config.git
        targetRevision: main
        path: "{{path}}"
      destination:
        server: https://kubernetes.default.svc
        namespace: "platform-{{path.basename}}"
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
          - ServerSideApply=true
        retry:
          limit: 5
          backoff:
            duration: 10s
            factor: 2
            maxDuration: 5m
kustomization.yaml
# Apply both files together:
#   kubectl apply -k .
#
# Order matters. The project has to exist before the ApplicationSet that
# references it, and kustomize keeps them in the order listed here.
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: argocd

resources:
  - project.yaml
  - applicationset.yaml

# labels with includeSelectors false adds the labels without touching selector
# fields. The older commonLabels rewrites selectors too, and selectors on an
# existing Deployment cannot be changed, so the apply fails.
labels:
  - pairs:
      app.kubernetes.io/managed-by: argocd
      owner: platform-engineering
    includeSelectors: false
project.yaml
# gitops-bootstrap / kubernetes / t1 "Standard"
#
# An AppProject is a fence. It says which repositories this set of applications
# may come from, which clusters and namespaces it may write to, and which kinds
# of object it may create. Without one, every Application runs with whatever
# Argo CD itself can do, which is usually everything.
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: platform
  namespace: argocd
spec:
  description: Platform services owned by the infrastructure team.

  # Only these repositories. A compromised repository somewhere else cannot
  # deploy into this project.
  sourceRepos:
    - https://github.com/your-org/your-cluster-config.git
    - https://falcosecurity.github.io/charts

  destinations:
    - server: https://kubernetes.default.svc
      namespace: platform-*
    - server: https://kubernetes.default.svc
      namespace: argocd
    # Falco runs in its own namespace, as its chart expects.
    - server: https://kubernetes.default.svc
      namespace: falco

  # Cluster-wide objects this project may create. Anything not listed is denied.
  #
  # Be clear about what this does and does not buy. ClusterRoleBinding is on the
  # list because Falco needs one, and anything allowed to create a
  # ClusterRoleBinding can bind itself to cluster-admin. What actually protects
  # you here is sourceRepos above: only the listed repositories can put anything
  # into this project. If a service does not need cluster-wide RBAC, give it a
  # project without these two lines.
  clusterResourceWhitelist:
    - group: ""
      kind: Namespace
    - group: rbac.authorization.k8s.io
      kind: ClusterRole
    - group: rbac.authorization.k8s.io
      kind: ClusterRoleBinding
    - group: apiextensions.k8s.io
      kind: CustomResourceDefinition

  namespaceResourceBlacklist:
    - group: ""
      kind: ResourceQuota
    - group: ""
      kind: LimitRange

  # Who can do what. sync is deliberately separate from override, so the people
  # who can deploy are not automatically the people who can bypass Git.
  roles:
    - name: deployer
      description: Can sync, cannot change the definition.
      policies:
        - p, proj:platform:deployer, applications, sync, platform/*, allow
        - p, proj:platform:deployer, applications, get, platform/*, allow
      groups:
        - your-org:platform-engineers
HardenedPlanned
Not written yet.
What catches people

Without the resources-finalizer, deleting the bootstrap Application leaves every application it created still running and managed by nobody. Both templates set it.

An AppProject narrows what an application can create. It does not make ClusterRoleBinding safe. Anything allowed to create one can bind itself to cluster-admin, and Falco needs one. The Standard project allows it, says so in a comment, and relies on the list of permitted repositories to keep strangers out.

How to undo it

kubectl delete on the Application removes it and, because of that finalizer, everything underneath it. Check that is what you meant before you press return.

What it costs

None beyond what the applications themselves use.

Registry 0.6.0. Built 2026-09-22.

Made by Habibullah Tora. Code under the MIT licence, writing under CC BY 4.0.