Artifacts

Securing Azure Artifacts: Permissions and Access

A comprehensive guide to securing Azure Artifacts feeds with role-based permissions, service account management, PAT scoping, pipeline authentication, audit logging, and access control best practices.

Securing Azure Artifacts: Permissions and Access

Overview

Azure Artifacts permissions are deceptively simple on the surface -- four roles, a handful of settings -- but getting them wrong creates either security holes or productivity blockers. A feed with overly broad permissions lets anyone publish packages, including compromised build agents and disgruntled contractors. A feed locked down too tightly breaks CI pipelines at 2 AM and generates urgent support tickets. The right configuration gives developers read access, build pipelines publish access, and administrators management access -- nothing more.

I have audited Azure Artifacts security for organizations that discovered, months after the fact, that a contractor's PAT still had write access to their production package feed. The default permissions in Azure DevOps are reasonable for small teams, but they do not scale to enterprises without deliberate configuration. This article covers the permission model, service account patterns, PAT scoping, pipeline authentication, audit logging, and the specific configurations I recommend for production environments.

Prerequisites

  • An Azure DevOps organization with Azure Artifacts enabled
  • Organization administrator or project administrator access
  • Existing Azure Artifacts feeds to secure
  • Familiarity with Azure DevOps security groups and identities
  • Node.js 18+ for the automation scripts

The Azure Artifacts Permission Model

Azure Artifacts uses a role-based access control (RBAC) model with four roles:

Role Read Packages Push Packages Unlist/Delete Manage Feed Settings
Reader Yes No No No
Collaborator Yes Yes (from upstream only) No No
Contributor Yes Yes Yes No
Owner Yes Yes Yes Yes

Understanding Each Role

Reader can install packages from the feed but cannot publish, unlist, or delete anything. This is the appropriate role for developers who consume packages but do not produce them, and for service accounts that only need to restore dependencies.

Collaborator is a specialized role that allows saving packages from upstream sources to the feed. When a developer installs a public package through an upstream source and the feed caches it, the collaborator role permits that caching operation. Collaborators cannot publish packages directly to the feed.

Contributor can publish, unlist, and deprecate packages. This is the role for build pipelines that publish packages and for developers who actively maintain packages.

Owner has full control including feed settings, permissions, upstream source configuration, and retention policies. Limit this to feed administrators.

Default Permissions

When you create a new feed, Azure DevOps applies these defaults:

  • Project Collection Valid Users: Reader (organization-scoped feeds)
  • [Project] Valid Users: Reader (project-scoped feeds)
  • Project Collection Administrators: Owner
  • Project Administrators: Owner (project-scoped feeds)
  • [Project] Build Service: Contributor

The build service default is important -- it means any pipeline in the project can publish to the feed. For many teams this is fine. For organizations with strict change control, you may want to restrict which pipelines can publish.

Managing Feed Permissions

Through the UI

Navigate to Artifacts > [Feed Name] > Feed Settings (gear icon) > Permissions. From here you can add users, groups, and service accounts, and assign roles.

Through the REST API

For repeatable configuration, manage permissions programmatically:

// manage-permissions.js
var https = require("https");

var org = process.env.AZURE_DEVOPS_ORG || "my-organization";
var project = process.env.AZURE_DEVOPS_PROJECT || "my-project";
var feedId = process.env.FEED_NAME || "my-packages";
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

function apiRequest(method, hostname, path, body, callback) {
  var options = {
    hostname: hostname,
    path: path,
    method: method,
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Basic " + auth
    }
  };
  if (body) {
    var bodyStr = JSON.stringify(body);
    options.headers["Content-Length"] = Buffer.byteLength(bodyStr);
  }

  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() { callback(null, res.statusCode, data); });
  });
  req.on("error", function(err) { callback(err); });
  if (body) req.write(JSON.stringify(body));
  req.end();
}

function getPermissions(callback) {
  var path = "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedId +
    "/permissions?api-version=7.1";
  apiRequest("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
    if (err) return callback(err);
    callback(null, JSON.parse(data));
  });
}

function setPermission(identityDescriptor, role, callback) {
  var path = "/" + org + "/" + project + "/_apis/packaging/feeds/" + feedId +
    "/permissions?api-version=7.1";

  var body = [
    {
      identityDescriptor: identityDescriptor,
      role: role
    }
  ];

  apiRequest("PATCH", "feeds.dev.azure.com", path, body, function(err, status, data) {
    if (err) return callback(err);
    if (status === 200) {
      callback(null, true);
    } else {
      callback(new Error("Failed (" + status + "): " + data));
    }
  });
}

function listPermissions() {
  getPermissions(function(err, result) {
    if (err) return console.error("Error:", err.message);

    console.log("Permissions for feed: " + feedId);
    console.log("====================================");

    var roleNames = {
      reader: "Reader",
      collaborator: "Collaborator",
      contributor: "Contributor",
      administrator: "Owner"
    };

    (result.value || []).forEach(function(perm) {
      var identity = perm.identityDescriptor || {};
      var displayName = perm.displayName || identity.id || "Unknown";
      var role = roleNames[perm.role] || perm.role;
      console.log("  " + displayName + " --> " + role);
    });
  });
}

var command = process.argv[2];

switch (command) {
  case "list":
    listPermissions();
    break;
  case "grant":
    var identity = process.argv[3];
    var role = process.argv[4];
    if (!identity || !role) {
      console.error("Usage: node manage-permissions.js grant <identityDescriptor> <role>");
      process.exit(1);
    }
    setPermission(identity, role, function(err) {
      if (err) return console.error("Error:", err.message);
      console.log("Permission granted: " + identity + " --> " + role);
    });
    break;
  default:
    console.log("Usage:");
    console.log("  node manage-permissions.js list");
    console.log("  node manage-permissions.js grant <identity> <role>");
    console.log("");
    console.log("Roles: reader, collaborator, contributor, administrator");
}
node manage-permissions.js list

# Output:
# Permissions for feed: my-packages
# ====================================
#   [platform] Build Service (my-organization) --> Contributor
#   Project Collection Administrators --> Owner
#   platform Team --> Reader
#   release-pipeline-svc --> Contributor

Personal Access Token (PAT) Scoping

PATs are the most common authentication method for interacting with Azure Artifacts outside of pipelines. They are also the most common source of security incidents because they are over-scoped, never rotated, and shared between people.

Minimum Scope Principle

Azure DevOps PATs support granular scoping. For Azure Artifacts, the relevant scopes are:

Scope Allows
Packaging (Read) Install/restore packages
Packaging (Read & Write) Install, publish, unlist packages

Never create a PAT with "Full access" for feed operations. Every PAT should have the minimum scope needed for its purpose:

  • Developer workstation (restore only): Packaging (Read)
  • Developer workstation (publish): Packaging (Read & Write)
  • CI pipeline (restore only): Use $(System.AccessToken) instead
  • CI pipeline (publish): Use $(System.AccessToken) with feed permissions
  • External service integration: Packaging (Read) or (Read & Write) as needed

PAT Lifecycle Management

PATs should have short expiration periods and be rotated regularly:

// pat-audit.js -- Check PAT expiration across the organization
var https = require("https");

var org = process.env.AZURE_DEVOPS_ORG || "my-organization";
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

// Note: Listing PATs requires the Token Administration scope
// This endpoint works for the current user's tokens
var options = {
  hostname: "vssps.dev.azure.com",
  path: "/" + org + "/_apis/tokens/pats?api-version=7.1-preview.1",
  method: "GET",
  headers: {
    "Authorization": "Basic " + auth,
    "Accept": "application/json"
  }
};

var req = https.request(options, function(res) {
  var data = "";
  res.on("data", function(chunk) { data += chunk; });
  res.on("end", function() {
    if (res.statusCode !== 200) {
      console.error("Failed (" + res.statusCode + "):", data);
      return;
    }

    var result = JSON.parse(data);
    var tokens = result.patTokens || [];
    var now = new Date();

    console.log("PAT Audit Report");
    console.log("=================");
    console.log("Total tokens: " + tokens.length);
    console.log("");

    var expiringSoon = [];
    var expired = [];

    tokens.forEach(function(token) {
      var expiry = new Date(token.validTo);
      var daysLeft = Math.ceil((expiry - now) / (1000 * 60 * 60 * 24));
      var scopes = (token.scope || "").split(" ").join(", ");

      if (daysLeft < 0) {
        expired.push(token);
      } else if (daysLeft < 30) {
        expiringSoon.push({ token: token, daysLeft: daysLeft });
      }

      console.log("  " + token.displayName);
      console.log("    Scope: " + scopes);
      console.log("    Expires: " + expiry.toISOString().split("T")[0] +
        " (" + (daysLeft > 0 ? daysLeft + " days left" : "EXPIRED") + ")");
      console.log("");
    });

    if (expiringSoon.length > 0) {
      console.log("WARNING: " + expiringSoon.length + " token(s) expiring within 30 days");
    }
    if (expired.length > 0) {
      console.log("ALERT: " + expired.length + " token(s) have expired");
    }
  });
});

req.on("error", function(err) { console.error("Error:", err.message); });
req.end();

Replacing PATs with System Access Tokens

In pipelines, always prefer $(System.AccessToken) over PATs:

steps:
  - task: NuGetAuthenticate@1
    displayName: Authenticate with feed

  # System.AccessToken is automatically available
  # No PAT management needed
  - script: dotnet nuget push *.nupkg --source my-feed --api-key az
    displayName: Publish package

The system access token:

  • Is generated fresh for each pipeline run
  • Has permissions scoped to the build service identity
  • Cannot be extracted and reused outside the pipeline
  • Expires when the pipeline run ends

Pipeline Service Account Permissions

The Build Service Identity

Every Azure DevOps project has a build service identity: [ProjectName] Build Service (Organization). This identity is used by all pipelines in the project when they authenticate with Azure Artifacts.

For project-scoped feeds, the build service identity automatically gets Contributor access. For organization-scoped feeds, you must grant access explicitly:

// grant-build-service.js
var https = require("https");

var org = "my-organization";
var feedId = "shared-packages"; // organization-scoped feed
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

// First, find the build service identity for the project
var projectName = "my-project";

function grantBuildServiceAccess() {
  // The build service identity descriptor format
  var identityDescriptor = "Microsoft.TeamFoundation.ServiceIdentity;Build:" +
    org + "\\\\Project Collection Build Service Accounts";

  var body = [{
    identityDescriptor: identityDescriptor,
    role: "contributor"
  }];

  var path = "/" + org + "/_apis/packaging/feeds/" + feedId +
    "/permissions?api-version=7.1";

  var bodyStr = JSON.stringify(body);
  var options = {
    hostname: "feeds.dev.azure.com",
    path: path,
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Basic " + auth,
      "Content-Length": Buffer.byteLength(bodyStr)
    }
  };

  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() {
      if (res.statusCode === 200) {
        console.log("Build service granted Contributor access to " + feedId);
      } else {
        console.error("Failed (" + res.statusCode + "):", data);
      }
    });
  });

  req.write(bodyStr);
  req.end();
}

grantBuildServiceAccess();

Restricting Which Pipelines Can Publish

If you need to limit publishing to specific pipelines (not all pipelines in the project):

  1. Remove the default build service Contributor access from the feed
  2. Create a dedicated service connection or variable group with a scoped PAT
  3. Grant that service connection access only to the publishing pipelines
# pipeline with restricted publishing access
variables:
  - group: package-publishing-credentials

steps:
  - task: NuGetAuthenticate@1
    inputs:
      nuGetServiceConnections: package-feed-connection
    displayName: Authenticate with restricted credentials

  - script: dotnet nuget push *.nupkg --source my-feed --api-key az
    displayName: Publish (only this pipeline has access)

Audit Logging

Azure DevOps logs all feed operations. You can access audit logs through the organization settings or through the API.

Accessing Audit Logs

Navigate to Organization Settings > Auditing to view feed-related events:

  • Feed created/deleted
  • Permission changes
  • Package published/deleted/unlisted
  • Feed settings changed
  • Upstream source modifications

Programmatic Audit Log Access

// feed-audit.js
var https = require("https");

var org = process.env.AZURE_DEVOPS_ORG || "my-organization";
var pat = process.env.AZURE_DEVOPS_PAT;
var auth = Buffer.from(":" + pat).toString("base64");

// Query audit logs for packaging events in the last 7 days
var startDate = new Date();
startDate.setDate(startDate.getDate() - 7);

var path = "/" + org + "/_apis/audit/auditlog?api-version=7.1&startTime=" +
  startDate.toISOString() + "&skipAggregation=true";

var options = {
  hostname: "auditservice.dev.azure.com",
  path: path,
  method: "GET",
  headers: {
    "Authorization": "Basic " + auth,
    "Accept": "application/json"
  }
};

var req = https.request(options, function(res) {
  var data = "";
  res.on("data", function(chunk) { data += chunk; });
  res.on("end", function() {
    if (res.statusCode !== 200) {
      console.error("Failed (" + res.statusCode + "):", data);
      return;
    }

    var result = JSON.parse(data);
    var events = (result.decoratedAuditLogEntries || []).filter(function(entry) {
      return entry.areaOfChange === "Packaging" ||
        (entry.actionId || "").indexOf("Packaging") !== -1;
    });

    console.log("Packaging Audit Log (last 7 days)");
    console.log("==================================");
    console.log("Found " + events.length + " packaging events");
    console.log("");

    events.forEach(function(event) {
      var timestamp = new Date(event.timestamp).toLocaleString();
      var actor = event.actorDisplayName || "Unknown";
      var action = event.actionId || "Unknown action";
      var details = event.details || "";

      console.log("[" + timestamp + "] " + actor);
      console.log("  Action: " + action);
      if (details) console.log("  Details: " + details);
      console.log("");
    });
  });
});

req.on("error", function(err) { console.error("Error:", err.message); });
req.end();
node feed-audit.js

# Output:
# Packaging Audit Log (last 7 days)
# ==================================
# Found 12 packaging events
#
# [2/8/2026, 3:15:00 PM] Platform Build Service
#   Action: Packaging.PackageVersionPublished
#   Details: Published MyCompany.SDK 2.1.4567 to shared-packages
#
# [2/7/2026, 10:30:00 AM] Jane Smith
#   Action: Packaging.FeedPermissionsChanged
#   Details: Added data-team as Reader on shared-packages

Service Connection Security

For cross-organization feed access or external NuGet/npm registries, Azure DevOps uses service connections. These are more secure than PATs because:

  • They are managed centrally in project settings
  • Access can be restricted to specific pipelines
  • They support approval and check workflows
  • They do not expose credentials to pipeline authors

Creating a Service Connection for External Feeds

# Using a service connection for an external NuGet feed
steps:
  - task: NuGetAuthenticate@1
    inputs:
      nuGetServiceConnections: external-nuget-connection
    displayName: Authenticate with external feed

Configure the service connection in Project Settings > Service Connections > New Service Connection > NuGet. Provide the feed URL and credentials. Then restrict which pipelines can use it under the service connection's security settings.

Complete Working Example

This example implements a security hardening script that audits and configures permissions across all feeds in an organization:

// harden-feeds.js -- Security hardening for Azure Artifacts feeds
var https = require("https");

var org = process.env.AZURE_DEVOPS_ORG || "my-organization";
var pat = process.env.AZURE_DEVOPS_PAT;
var dryRun = process.argv.indexOf("--dry-run") !== -1;

if (!pat) {
  console.error("Error: AZURE_DEVOPS_PAT is required");
  process.exit(1);
}

var auth = Buffer.from(":" + pat).toString("base64");

function apiRequest(method, hostname, path, body, callback) {
  var options = {
    hostname: hostname,
    path: path,
    method: method,
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Basic " + auth
    }
  };
  if (body) {
    var bodyStr = JSON.stringify(body);
    options.headers["Content-Length"] = Buffer.byteLength(bodyStr);
  }
  var req = https.request(options, function(res) {
    var data = "";
    res.on("data", function(chunk) { data += chunk; });
    res.on("end", function() { callback(null, res.statusCode, data); });
  });
  req.on("error", function(err) { callback(err); });
  if (body) req.write(JSON.stringify(body));
  req.end();
}

function getFeeds(callback) {
  apiRequest("GET", "feeds.dev.azure.com",
    "/" + org + "/_apis/packaging/feeds?api-version=7.1", null,
    function(err, status, data) {
      if (err) return callback(err);
      callback(null, JSON.parse(data).value || []);
    });
}

function getFeedPermissions(feedId, feedProject, callback) {
  var path = feedProject ?
    "/" + org + "/" + feedProject + "/_apis/packaging/feeds/" + feedId + "/permissions?api-version=7.1" :
    "/" + org + "/_apis/packaging/feeds/" + feedId + "/permissions?api-version=7.1";
  apiRequest("GET", "feeds.dev.azure.com", path, null, function(err, status, data) {
    if (err) return callback(err);
    callback(null, JSON.parse(data).value || []);
  });
}

function auditFeed(feed, callback) {
  var feedProject = feed.project ? feed.project.name : null;
  var findings = [];

  getFeedPermissions(feed.id, feedProject, function(err, permissions) {
    if (err) {
      findings.push({ severity: "error", message: "Could not read permissions: " + err.message });
      return callback(findings);
    }

    // Check for overly broad write access
    permissions.forEach(function(perm) {
      var name = perm.displayName || "Unknown";
      var role = perm.role;

      // Flag if "Valid Users" or broad groups have contributor/owner access
      if ((name.indexOf("Valid Users") !== -1 || name.indexOf("Everyone") !== -1) &&
          (role === "contributor" || role === "administrator")) {
        findings.push({
          severity: "high",
          message: "Broad group '" + name + "' has " + role + " access -- " +
            "this allows any user in the scope to publish packages"
        });
      }

      // Flag if there are too many owners
      if (role === "administrator") {
        findings.push({
          severity: "info",
          message: "Owner: " + name
        });
      }
    });

    // Check upstream configuration
    if (!feed.upstreamEnabled) {
      findings.push({
        severity: "medium",
        message: "No upstream sources configured -- " +
          "developers may use extra-index-url/multiple sources, risking dependency confusion"
      });
    }

    // Check if hide deleted versions is enabled
    if (!feed.hideDeletedPackageVersions) {
      findings.push({
        severity: "low",
        message: "Deleted package versions are visible -- " +
          "consider hiding them to reduce confusion"
      });
    }

    callback(findings);
  });
}

function runAudit() {
  console.log("Azure Artifacts Security Audit");
  console.log("Organization: " + org);
  console.log("Mode: " + (dryRun ? "AUDIT ONLY" : "AUDIT + REMEDIATE"));
  console.log("==============================");
  console.log("");

  getFeeds(function(err, feeds) {
    if (err) return console.error("Error:", err.message);

    var totalFindings = { high: 0, medium: 0, low: 0, info: 0 };
    var completed = 0;

    feeds.forEach(function(feed) {
      auditFeed(feed, function(findings) {
        completed++;
        var scope = feed.project ? "Project (" + feed.project.name + ")" : "Organization";

        console.log("Feed: " + feed.name + " [" + scope + "]");

        findings.forEach(function(f) {
          var prefix = {
            high: "[HIGH]  ",
            medium: "[MED]   ",
            low: "[LOW]   ",
            info: "[INFO]  ",
            error: "[ERROR] "
          }[f.severity] || "[???]   ";

          console.log("  " + prefix + f.message);
          if (totalFindings[f.severity] !== undefined) {
            totalFindings[f.severity]++;
          }
        });

        if (findings.length === 0) {
          console.log("  No findings.");
        }
        console.log("");

        if (completed === feeds.length) {
          console.log("==============================");
          console.log("Summary:");
          console.log("  Feeds audited: " + feeds.length);
          console.log("  High findings: " + totalFindings.high);
          console.log("  Medium findings: " + totalFindings.medium);
          console.log("  Low findings: " + totalFindings.low);

          if (totalFindings.high > 0) {
            console.log("");
            console.log("ACTION REQUIRED: " + totalFindings.high +
              " high-severity finding(s) need immediate attention.");
          }
        }
      });
    });
  });
}

runAudit();
node harden-feeds.js --dry-run

# Output:
# Azure Artifacts Security Audit
# Organization: my-organization
# Mode: AUDIT ONLY
# ==============================
#
# Feed: shared-packages [Organization]
#   [INFO]  Owner: Project Collection Administrators
#   [INFO]  Owner: Platform Admins
#   No high findings.
#
# Feed: dev-packages [Project (platform)]
#   [HIGH]  Broad group 'Project Valid Users' has contributor access --
#           this allows any user in the scope to publish packages
#   [MED]   No upstream sources configured -- developers may use
#           extra-index-url/multiple sources, risking dependency confusion
#
# Feed: legacy-feed [Organization]
#   [HIGH]  Broad group 'Project Collection Valid Users' has contributor access
#   [LOW]   Deleted package versions are visible
#
# ==============================
# Summary:
#   Feeds audited: 3
#   High findings: 2
#   Medium findings: 1
#   Low findings: 1
#
# ACTION REQUIRED: 2 high-severity finding(s) need immediate attention.

Common Issues and Troubleshooting

1. Pipeline Gets 403 When Publishing to Organization Feed

Error:

403 Forbidden: The current user does not have permission to publish to this feed.

The project build service identity does not have Contributor access on the organization-scoped feed. Navigate to the feed's permissions and add [ProjectName] Build Service (org) as a Contributor. For organization-scoped feeds, you may also need Project Collection Build Service Accounts as a Contributor.

2. PAT Authentication Stops Working After Scope Change

Error:

401 Unauthorized after updating PAT permissions

When you modify a PAT's scope, the change may not take effect immediately. Azure DevOps caches PAT permissions for up to 60 minutes. Wait and retry, or regenerate the PAT entirely.

3. Cannot Remove Default Permissions

Error: Attempting to remove Project Collection Valid Users from a feed returns an error.

Some default permissions are inherited and cannot be removed. You can override them by setting the role to a more restrictive level, but you cannot remove the identity entirely. For project-scoped feeds, the project valid users group always has at least Reader access.

4. Service Connection Not Working in Pipeline

Error:

The service connection 'package-feed-connection' could not be found or has not been authorized for use.

Service connections must be authorized for each pipeline that uses them. Navigate to Project Settings > Service Connections > [connection] > Security and add the pipeline or allow all pipelines. Also verify the connection's credentials have not expired.

5. Audit Logs Missing Package Events

Error: Package publishing events do not appear in the audit log.

Azure DevOps audit logging must be enabled at the organization level. Navigate to Organization Settings > Policies > Auditing and ensure it is turned on. Audit log retention is 90 days by default. Events may take up to 30 minutes to appear.

6. Cross-Project Feed Access Breaks After Permissions Change

Error: A pipeline in Project B can no longer restore packages from a feed in Project A.

Verify that the build service identity for Project B has Reader (or Collaborator) access on Project A's feed. When Azure DevOps updates security groups, cached permissions may take up to an hour to refresh. In the meantime, clearing the NuGet cache (dotnet nuget locals all --clear) can force re-authentication.

Best Practices

  1. Use $(System.AccessToken) in pipelines, not PATs. System access tokens are ephemeral, scoped, and cannot be extracted. PATs persist on disk, can be shared, and are a common attack vector.

  2. Scope PATs to the minimum required permission. A PAT for restoring packages needs only Packaging (Read). A PAT for publishing needs Packaging (Read & Write). Never use Full Access tokens for feed operations.

  3. Set PAT expiration to 90 days maximum. Short-lived tokens reduce the window of exposure if a token is compromised. Combine with calendar reminders to rotate before expiration.

  4. Remove the default Contributor access for broad groups. On production feeds, remove [Project] Valid Users as Contributor and add only specific build service identities and teams that need publish access.

  5. Use security groups instead of individual users. Create Azure DevOps groups like "Package Publishers" and "Feed Administrators" and assign roles to groups. This scales better than managing individual user permissions.

  6. Audit feed permissions quarterly. Run the audit script or manually review permissions every quarter. Remove access for departed employees, decommissioned service accounts, and archived projects.

  7. Restrict feed Owner access to two or three administrators. Every Owner can change feed settings, modify permissions, and delete packages. Too many Owners increases the risk of accidental misconfiguration.

  8. Enable organization-level audit logging. Audit logs provide a trail for incident investigation. If a malicious package is published, audit logs tell you who published it, when, and from which IP address.

  9. Use service connections for cross-organization access. When consuming packages from feeds outside your organization, use service connections instead of PATs. Service connections are centrally managed, can be restricted to specific pipelines, and support approval workflows.

  10. Separate publish permissions from consume permissions. Most developers should have Reader access. Only build pipelines and package maintainers need Contributor access. This limits the blast radius if a developer's credentials are compromised.

References

Powered by Contentful