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:
- Golden Paths — caminos dorados que guían a los desarrolladores por el flujo correcto.
- 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:
| Herramienta | Versión | Para qué |
|---|---|---|
| Backstage | v1.49+ | Portal de desarrolladores |
| Node.js | 22+ | Runtime de Backstage |
| Kubernetes | 1.28+ | Cluster destino |
| kubectl | 1.28+ | Acceso al cluster |
Y si vienes de los posts de la serie Backstage, ya tienes la mayor parte del setup listo.
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
environmentsolo ofrecedevystaging. Nadie puede desplegar en producción desde este template. - El
namespacese controla a nivel del template, no lo escribe el usuario libremente. - El
ownerusaOwnerPickerque 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: 5Fí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:
- Permission: Representa una acción concreta, como
catalog.entity.readoscaffolder.task.create. - Policy: La función que evalúa si un usuario puede realizar esa acción. Puede retornar
ALLOW,DENYoCONDITIONAL. - 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: trueLuego instala el módulo de permisos en el backend:
yarn workspace backend add @backstage/plugin-permission-backend-module-allow-all-policyallow-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-backendReemplaza 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.csvCrea 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, allowEsto 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-paymentsFí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: backstageAplícalos:
kubectl apply -f namespaces.yamlY 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: NetworkPolicyCon 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:
- Backstage RBAC valida que el usuario puede ejecutar el template.
- El template restringe los namespaces disponibles según el equipo.
- ArgoCD AppProject valida que el destino es un namespace permitido.
- 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
| Capa | Herramienta | Qué controla |
|---|---|---|
| 1. Acceso al template | Backstage RBAC | Quién puede ejecutar qué template |
| 2. Parámetros permitidos | Golden Path Template | Qué namespaces y entornos están disponibles |
| 3. Destino del deploy | ArgoCD AppProject | A dónde puede sincronizar cada proyecto |
| 4. Permisos en el cluster | Kubernetes RBAC | Qué 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.