This episode secures authorization: method-level access control, centralized policy enforcement, audit logging and protection of sensitive metadata, plus rate limiting, circuit breaking, and protection from Denial of Service attacks on gRPC services.

Authentication answers "who are you?". Authorization answers the more important question: "what are you allowed to do?". A legitimate user isn't necessarily allowed to delete products. Episode 12 builds the authorization, audit, and defense layers that make a gRPC service truly secure.
We cover method-level access control, centralized policy enforcement, audit logging that records who did what, protection of sensitive metadata, and then defenses against abuse: rate limiting, circuit breaking, and DoS mitigation.
The most common pattern: Role-Based Access Control (RBAC). Claims inside the JWT (episode 11) are mapped to roles, then roles are mapped to the methods they're allowed to call:
var accessRules = map[string]string{
"catalog.v1.CatalogService/GetProduct": "user",
"catalog.v1.CatalogService/ListProducts": "user",
"catalog.v1.CatalogService/AddBulk": "admin",
"catalog.v1.CatalogService/UpdatePrice": "admin",
}With the map above, GetProduct can be called by the user role, while AddBulk and UpdatePrice are admin-only. Methods not listed can be given a default policy — for example, denied.
Enforcement happens in a server interceptor so one place controls the entire service:
func authorize(ctx context.Context, fullMethod string) error {
role := roleFromClaims(ctx)
required, ok := accessRules[fullMethod]
if !ok {
return status.Error(codes.PermissionDenied, "method tidak dikenali")
}
if role != required && role != "superadmin" {
return status.Error(codes.PermissionDenied, "hak akses tidak cukup")
}
return nil
}The authorize(ctx, fullMethod) function reads the role from the claims, matches it against the map, and denies with codes.PermissionDenied if it doesn't match. Call this function before handler in the server interceptor.
Hardcoded policies are hard to change and hard to audit. Move them to external configuration — a YAML file read at startup:
methods:
- path: catalog.v1.CatalogService/GetProduct
roles: [user, admin, superadmin]
- path: catalog.v1.CatalogService/AddBulk
roles: [admin, superadmin]
- path: catalog.v1.CatalogService/UpdatePrice
roles: [admin, superadmin]With roles: [admin, superadmin] in a separate file, policy changes are just a new configuration deploy without recompiling the server. This approach grows into the Authorization Policy managed by a service mesh in episode 18.
Every important data change must be recorded. A unary interceptor can capture the full call context:
func auditInterceptor(ctx context.Context, req any,
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
identity := identityFromContext(ctx)
res, err := handler(ctx, req)
audit.Log(identity, info.FullMethod, req, err)
return res, err
}audit.Log(identity, info.FullMethod, req, err) writes an audit line: who called, which method, what payload, and the result. Store it in append-only storage that callers can't modify — for example a centralized log or object storage.
Never write tokens or credentials into logs. Tokens live in the authorization metadata, not the payload — make sure audit logging doesn't include raw metadata. If needed for investigation, hash it first or truncate part of it.
Rate limiting prevents one client from flooding the server. A token bucket grants a request quota per second per identity:
func rateLimit(identity string) error {
limiter, ok := buckets[identity]
if !ok {
limiter = rate.NewLimiter(rate.Limit(20), 40)
buckets[identity] = limiter
}
if !limiter.Allow() {
return status.Error(codes.ResourceExhausted, "terlalu banyak request")
}
return nil
}rate.NewLimiter(rate.Limit(20), 40) allows 20 requests per second with a burst of 40. Violations are answered with codes.ResourceExhausted so the client knows to wait, not just that it failed.
Three more defense layers that are mandatory:
MaxRecvMsgSize so giant messages can't exhaust memory.s := grpc.NewServer(
grpc.MaxRecvMsgSize(4*1024*1024),
grpc.MaxSendMsgSize(4*1024*1024),
)The MaxRecvMsgSize(4*1024*1024) setting caps incoming messages at 4 MiB. This combination holds off application-level DoS attacks without waiting for the network to break.
Key takeaways:
ResourceExhausted.In episode 13 next, we cover gRPC performance and optimization — measuring latency and throughput with ghz, optimizing through compression and connection reuse, handling backpressure, and composing efficient protobuf messages with packed fields and repeated fields. Your solid security is now balanced with measurable speed.