Contenido

Backstage: Golden Paths, RBAC y cómo controlar quién despliega dónde

El problema: libertad sin control = caos

Imagínate este escenario: tienes 5 equipos de desarrollo, cada uno con 3 o 4 microservicios. Todos tienen acceso a Backstage y pueden crear templates, generar manifiestos y desplegar en Kubernetes. Suena bien, ¿no?

Hasta que el equipo de pagos despliega por error en el namespace de producción de facturación. O alguien del equipo de frontend hace un kubectl apply directo al cluster de staging sin pasar por el flujo de GitOps. O peor: un junior recién llegado borra un ConfigMap de otro equipo porque “pensaba que era de prueba”.

¿Te suena familiar? Entonces necesitas dos cosas:

  1. Golden Paths — caminos dorados que guían a los desarrolladores por el flujo correcto.
  2. RBAC — control de acceso para que cada quien solo pueda hacer lo que le corresponde.

¿Qué son los Golden Paths?

Un Golden Path es un camino pre-definido y opinado que guía a los desarrolladores hacia las mejores prácticas de tu organización. Piensa en él como el “camino feliz” pero institucionalizado.

En vez de decirle a un developer “crea un repo, configura el CI, escribe los manifiestos de Kubernetes, registra el servicio en el catálogo y configura ArgoCD”, le dices: “llena este formulario y nosotros nos encargamos del resto”.

¿Por qué son tan importantes?

  • Onboarding rápido: Un developer nuevo puede desplegar su primer servicio el primer día.
  • Consistencia: Todos los servicios siguen la misma estructura, naming, labels y annotations.
  • Seguridad por diseño: El template ya incluye las políticas de red, resource limits y RBAC de Kubernetes.
  • Menos errores: No hay espacio para “me olvidé de agregar el health check” o “no le puse resource limits”.

Pre-requisitos

Antes de seguir, necesitas tener:

HerramientaVersiónPara qué
Backstagev1.49+Portal de desarrolladores
Node.js22+Runtime de Backstage
Kubernetes1.28+Cluster destino
kubectl1.28+Acceso al cluster

Y si vienes de los posts de la serie Backstage, ya tienes la mayor parte del setup listo.

Tip
Este post es independiente de la serie Backstage, pero si quieres el contexto completo, te recomiendo revisar la Series #1 para la instalación inicial.

Paso 1: Crear un Golden Path Template

Vamos a crear un template que permita a un developer desplegar un servicio nuevo de forma estandarizada. La clave está en que el template controla en qué namespace se puede desplegar, basado en el equipo del usuario.

Crea el archivo templates/golden-path-service/template.yaml:

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: golden-path-service
  title: "🛤️ Golden Path: Nuevo Servicio"
  description: Crea un servicio estandarizado con repo, CI, manifiestos K8s y registro en catálogo
  tags:
    - golden-path
    - kubernetes
    - recommended
spec:
  owner: group:default/platform-team
  type: service

  parameters:
    - title: Información del servicio
      required:
        - name
        - owner
      properties:
        name:
          title: Nombre del servicio
          type: string
          description: Nombre en kebab-case (ej. api-payments)
          pattern: "^[a-z][a-z0-9-]*$"
          ui:autofocus: true
        description:
          title: Descripción
          type: string
        owner:
          title: Equipo owner
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group

    - title: Configuración de deployment
      required:
        - environment
        - namespace
      properties:
        environment:
          title: Entorno
          type: string
          enum:
            - dev
            - staging
          enumNames:
            - "Development"
            - "Staging"
          default: dev
        namespace:
          title: Namespace
          type: string
          description: El namespace se asigna según tu equipo
        replicas:
          title: Réplicas
          type: integer
          default: 2
          minimum: 1
          maximum: 5
        port:
          title: Puerto del servicio
          type: integer
          default: 8080

    - title: Repositorio
      required:
        - repoUrl
      properties:
        repoUrl:
          title: Ubicación del repo
          type: string
          ui:field: RepoUrlPicker
          ui:options:
            allowedHosts:
              - github.com

  steps:
    - id: fetch-template
      name: Generar estructura del proyecto
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          namespace: ${{ parameters.namespace }}
          environment: ${{ parameters.environment }}
          replicas: ${{ parameters.replicas }}
          port: ${{ parameters.port }}

    - id: publish-repo
      name: Crear repositorio en GitHub
      action: publish:github
      input:
        allowedHosts: ["github.com"]
        repoUrl: ${{ parameters.repoUrl }}
        description: ${{ parameters.description }}
        defaultBranch: main
        repoVisibility: internal

    - id: register-catalog
      name: Registrar en el catálogo
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps['publish-repo'].output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

  output:
    links:
      - title: Repositorio
        url: ${{ steps['publish-repo'].output.remoteUrl }}
      - title: Ver en catálogo
        icon: catalog
        entityRef: ${{ steps['register-catalog'].output.entityRef }}

Fíjate en algunos detalles importantes:

  • El campo environment solo ofrece dev y staging. Nadie puede desplegar en producción desde este template.
  • El namespace se controla a nivel del template, no lo escribe el usuario libremente.
  • El owner usa OwnerPicker que muestra solo los grupos del catálogo.

Paso 2: El esqueleto del proyecto (skeleton)

El template genera una estructura estándar. Crea la carpeta templates/golden-path-service/skeleton/ con estos archivos:

catalog-info.yaml

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: ${{ values.name }}
  description: ${{ values.description }}
  annotations:
    github.com/project-slug: ${{ values.repoUrl | projectSlug }}
    backstage.io/kubernetes-namespace: ${{ values.namespace }}
    argocd/app-name: ${{ values.name }}
  tags:
    - golden-path
spec:
  type: service
  lifecycle: experimental
  owner: ${{ values.owner }}

k8s/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${{ values.name }}
  namespace: ${{ values.namespace }}
  labels:
    app: ${{ values.name }}
    team: ${{ values.owner | replace("group:default/", "") }}
    managed-by: backstage
spec:
  replicas: ${{ values.replicas }}
  selector:
    matchLabels:
      app: ${{ values.name }}
  template:
    metadata:
      labels:
        app: ${{ values.name }}
        team: ${{ values.owner | replace("group:default/", "") }}
    spec:
      containers:
        - name: ${{ values.name }}
          image: ghcr.io/your-org/${{ values.name }}:latest
          ports:
            - containerPort: ${{ values.port }}
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          livenessProbe:
            httpGet:
              path: /health
              port: ${{ values.port }}
            initialDelaySeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: ${{ values.port }}
            initialDelaySeconds: 5

Fíjate que el skeleton ya incluye:

  • Resource limits (no opcionales, siempre van).
  • Health checks (liveness y readiness).
  • Labels de equipo (para filtrar en dashboards y políticas de red).
  • Namespace fijo basado en el parámetro del template.

Esto es el poder de un Golden Path: las mejores prácticas están embebidas, no dependen de que el developer se acuerde.


Paso 3: Entender el Permission Framework

Antes de configurar RBAC, necesitas entender cómo funciona el sistema de permisos de Backstage. Tiene tres conceptos clave:

  1. Permission: Representa una acción concreta, como catalog.entity.read o scaffolder.task.create.
  2. Policy: La función que evalúa si un usuario puede realizar esa acción. Puede retornar ALLOW, DENY o CONDITIONAL.
  3. Resource: El recurso sobre el que se aplica la acción (un template, una entidad del catálogo, etc.).

El truco está en las decisiones condicionales: en vez de un simple sí/no, puedes decir “sí, pero solo si el usuario es owner de la entidad” o “sí, pero solo para entidades con cierta annotation”.


Paso 4: Activar el Permission Framework

Primero, habilita el sistema de permisos en app-config.yaml:

permission:
  enabled: true

Luego instala el módulo de permisos en el backend:

yarn workspace backend add @backstage/plugin-permission-backend-module-allow-all-policy
Importante
El módulo allow-all-policy es solo para empezar. En producción nunca debes usar una política que permita todo. Lo reemplazaremos en el siguiente paso.

Regístralo en packages/backend/src/index.ts:

backend.add(
  import('@backstage/plugin-permission-backend-module-allow-all-policy'),
);

Paso 5: Configurar RBAC

Ahora sí, vamos a configurar permisos reales. Instala el plugin de RBAC:

yarn workspace backend add @backstage-community/plugin-rbac-backend

Reemplaza el módulo allow-all-policy en packages/backend/src/index.ts:

// Quita esto:
// backend.add(import('@backstage/plugin-permission-backend-module-allow-all-policy'));

// Agrega esto:
backend.add(import('@backstage-community/plugin-rbac-backend'));

Definir roles y políticas

Configura los roles en app-config.yaml:

permission:
  enabled: true
  rbac:
    admin:
      superUsers:
        - name: group:default/platform-team
    pluginsWithPermission:
      - catalog
      - scaffolder
    policies-csv-file: ./rbac-policies.csv

Crea el archivo rbac-policies.csv con las políticas:

# Formato: tipo_de_política, sujeto, permiso, efecto
# p = policy (regla de permiso)
# g = group (asignación de rol)

# Roles
g, group:default/team-payments, role:default/payments-deployer
g, group:default/team-frontend, role:default/frontend-deployer
g, group:default/platform-team, role:default/platform-admin

# Platform admins pueden todo
p, role:default/platform-admin, catalog.entity.read, allow
p, role:default/platform-admin, catalog.entity.create, allow
p, role:default/platform-admin, catalog.entity.delete, allow
p, role:default/platform-admin, scaffolder.template.read, allow
p, role:default/platform-admin, scaffolder.task.create, allow
p, role:default/platform-admin, scaffolder.task.cancel, allow
p, role:default/platform-admin, scaffolder.task.read, allow

# Deployers pueden ver el catálogo y ejecutar templates
p, role:default/payments-deployer, catalog.entity.read, allow
p, role:default/payments-deployer, scaffolder.template.read, allow
p, role:default/payments-deployer, scaffolder.task.create, allow
p, role:default/payments-deployer, scaffolder.task.read, allow

p, role:default/frontend-deployer, catalog.entity.read, allow
p, role:default/frontend-deployer, scaffolder.template.read, allow
p, role:default/frontend-deployer, scaffolder.task.create, allow
p, role:default/frontend-deployer, scaffolder.task.read, allow

Esto establece que:

  • platform-team puede hacer todo (son los admins de la plataforma).
  • team-payments puede ver el catálogo y ejecutar templates, pero no borrar entidades.
  • team-frontend tiene los mismos permisos que payments (ver y ejecutar).

Paso 6: Restringir namespaces por equipo

Aquí es donde se pone interesante. Queremos que cada equipo solo pueda desplegar en su namespace. Para lograr esto, combinamos dos estrategias:

Estrategia 1: Namespaces predefinidos en el template

La forma más simple y directa es que el template defina los namespaces válidos por equipo:

# En el template, reemplaza el campo namespace con esto:
namespace:
  title: Namespace
  type: string
  description: Namespace asignado a tu equipo
  oneOf:
    - const: payments-dev
      title: "payments-dev (Team Payments)"
    - const: payments-staging
      title: "payments-staging (Team Payments)"
    - const: frontend-dev
      title: "frontend-dev (Team Frontend)"
    - const: frontend-staging
      title: "frontend-staging (Team Frontend)"

Esto funciona, pero tiene un problema: cualquier usuario puede elegir cualquier namespace del dropdown. Para realmente restringirlo, necesitas la estrategia 2.

Estrategia 2: Templates separados por equipo

Crea un template específico por equipo con los namespaces hardcodeados:

# template-payments.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: golden-path-payments
  title: "🛤️ Golden Path: Servicio de Pagos"
  tags:
    - golden-path
    - payments
spec:
  owner: group:default/team-payments
  type: service

  parameters:
    - title: Información del servicio
      properties:
        name:
          title: Nombre del servicio
          type: string
          pattern: "^[a-z][a-z0-9-]*$"
        environment:
          title: Entorno
          type: string
          enum: [dev, staging]
          default: dev

  steps:
    - id: fetch-template
      name: Generar proyecto
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          # El namespace se calcula automáticamente
          namespace: payments-${{ parameters.environment }}
          owner: group:default/team-payments

Fíjate: el namespace no es un parámetro libre. Se calcula como payments-{environment}. El equipo de pagos solo puede desplegar en payments-dev o payments-staging. Punto.

Estrategia 3: Política condicional con el Permission Framework

Para una solución más avanzada, puedes escribir una política custom que valide el acceso a nivel de grupo. Este ejemplo verifica que el usuario pertenezca a un grupo con namespaces asignados antes de permitir la creación de tareas del scaffolder:

// plugins/permission-backend-module-custom/src/policy.ts
import {
  PolicyDecision,
  AuthorizeResult,
} from '@backstage/plugin-permission-common';
import {
  PermissionPolicy,
  PolicyQuery,
} from '@backstage/plugin-permission-node';
import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';

// Mapeo de grupos a namespaces permitidos
const NAMESPACE_POLICY: Record<string, string[]> = {
  'group:default/team-payments': ['payments-dev', 'payments-staging'],
  'group:default/team-frontend': ['frontend-dev', 'frontend-staging'],
  'group:default/platform-team': ['*'], // Acceso a todo
};

export class CustomPermissionPolicy implements PermissionPolicy {
  async handle(
    request: PolicyQuery,
    user?: BackstageIdentityResponse,
  ): Promise<PolicyDecision> {
    // Obtener los grupos del usuario
    const userGroups = user?.identity.ownershipEntityRefs ?? [];

    // Si es scaffolder.task.create, verificar namespace
    if (request.permission.name === 'scaffolder.task.create') {
      const hasAccess = userGroups.some(group => {
        const allowed = NAMESPACE_POLICY[group];
        return allowed?.includes('*') || allowed?.length > 0;
      });

      return {
        result: hasAccess
          ? AuthorizeResult.ALLOW
          : AuthorizeResult.DENY,
      };
    }

    // Para todo lo demás, delegar al RBAC
    return { result: AuthorizeResult.ALLOW };
  }
}

Paso 7: Definir los namespaces en Kubernetes

Para que todo encaje, necesitas que los namespaces existan en el cluster con las labels correctas:

# namespaces.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: payments-dev
  labels:
    team: team-payments
    environment: dev
    managed-by: backstage
---
apiVersion: v1
kind: Namespace
metadata:
  name: payments-staging
  labels:
    team: team-payments
    environment: staging
    managed-by: backstage
---
apiVersion: v1
kind: Namespace
metadata:
  name: frontend-dev
  labels:
    team: team-frontend
    environment: dev
    managed-by: backstage
---
apiVersion: v1
kind: Namespace
metadata:
  name: frontend-staging
  labels:
    team: team-frontend
    environment: staging
    managed-by: backstage

Aplícalos:

kubectl apply -f namespaces.yaml

Y si usas ArgoCD, puedes restringir las Applications a namespaces específicos con AppProject:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-payments
  namespace: argocd
spec:
  description: Proyecto para el equipo de pagos
  sourceRepos:
    - "https://github.com/your-org/payments-*"
  destinations:
    - namespace: payments-dev
      server: https://kubernetes.default.svc
    - namespace: payments-staging
      server: https://kubernetes.default.svc
  # Bloquear recursos sensibles
  namespaceResourceBlacklist:
    - group: ""
      kind: ResourceQuota
    - group: ""
      kind: LimitRange
    - group: ""
      kind: NetworkPolicy

Con el AppProject, ArgoCD rechaza cualquier intento de desplegar fuera de payments-dev o payments-staging. Es una segunda capa de seguridad que complementa lo que hace Backstage.


El flujo completo

Así se ve el flujo de punta a punta con todas las piezas conectadas:

Cada paso tiene su validación:

  1. Backstage RBAC valida que el usuario puede ejecutar el template.
  2. El template restringe los namespaces disponibles según el equipo.
  3. ArgoCD AppProject valida que el destino es un namespace permitido.
  4. Kubernetes RBAC (RoleBindings) limita qué puede hacer el ServiceAccount de ArgoCD en ese namespace.

Son cuatro capas de seguridad, cada una complementando a la otra.


Resumen

CapaHerramientaQué controla
1. Acceso al templateBackstage RBACQuién puede ejecutar qué template
2. Parámetros permitidosGolden Path TemplateQué namespaces y entornos están disponibles
3. Destino del deployArgoCD AppProjectA dónde puede sincronizar cada proyecto
4. Permisos en el clusterKubernetes RBACQué operaciones puede hacer cada ServiceAccount

Los Golden Paths no son solo “templates bonitos”. Son la forma de codificar las políticas de tu organización en un flujo de autoservicio. El developer no necesita saber que hay cuatro capas de seguridad por debajo — solo llena un formulario y todo funciona.

Eso es Platform Engineering: hacer que lo correcto sea lo más fácil de hacer.


Recursos