Building a DevSecOps pipelineModule 1: Foundations and your lab repository

Lab: set up the course repository and its first pipeline

Create devsecops-lab with a small Flask service, pinned dependencies and the pre-commit framework, then add a first workflow with a read-only token and a smoke test that every later lab builds on.

This lesson
QuestionsPass 7/10
Course26 items

In this lab you will

  • Create the devsecops-lab repository with a minimal Flask service that later labs scan, package and deploy
  • Pin application dependencies and keep developer tools in a separate requirements file
  • Install the pre-commit framework with a few basic hooks
  • Add a first workflow with a top-level read-only token, a timeout and a smoke test

In this lab you create the repository that every later lab adds to. By the end, you have a working service, pinned dependencies, a pre-commit setup ready for the secret scanner in module 2, and a first green pipeline.

Before you start

You need:

  • a free GitHub account, with the GitHub CLI (gh) installed and signed in (gh auth login)
  • Git and Python 3.11 or newer
  • a Bash shell: Linux, macOS, or WSL with Ubuntu on Windows

Make the repository public. On a free account, branch rules for private repositories are limited, and public repositories run GitHub Actions on standard hosted runners at no cost. Nothing secret goes into this repository.

Step 1: create the repository and the service

  1. Create the repository and clone it:

    gh repo create devsecops-lab --public --clone
    cd devsecops-lab
  2. Create a virtual environment and install Flask:

    python3 -m venv .venv
    source .venv/bin/activate
    pip install flask
    pip freeze > requirements.txt
    cat requirements.txt

    pip freeze writes the exact version of Flask and every library it pulls in. Do this before installing any developer tools, so requirements.txt lists only what the application needs at runtime.

  3. Create app.py:

    from flask import Flask, jsonify, request
    
    app = Flask(__name__)
    
    
    @app.get("/health")
    def health():
        return jsonify(status="ok")
    
    
    @app.get("/hello")
    def hello():
        # Cap the input length; never trust what the client sends.
        name = request.args.get("name", "world")[:50]
        return jsonify(message=f"Hello, {name}")
    
    
    if __name__ == "__main__":
        app.run(host="127.0.0.1", port=8000)
  4. Run it and check it answers:

    python app.py &
    sleep 2
    curl -s http://127.0.0.1:8000/health
    curl -s "http://127.0.0.1:8000/hello?name=lab"
    kill %1

    You should see {"status":"ok"} and {"message":"Hello, lab"}.

Step 2: keep the wrong files out

  1. Create .gitignore:

    .venv/
    __pycache__/
    .env
    .env.*
    !.env.example
    *.pem
    *.key

    Ignoring .env means a local file of real credentials is never staged by git add .. The !.env.example line lets you commit an example file with placeholder values so others know which variables exist.

  2. Check it works:

    echo "DB_PASSWORD=not-a-real-password" > .env
    git status --short    # .env must not appear
    rm .env

Step 3: set up the pre-commit framework

pre-commit runs checks before Git creates a commit. You add the secret scanner in module 2; for now, set up the framework with a few general hooks.

  1. Install it and pin it in a separate developer requirements file:

    pip install pre-commit
    pip show pre-commit | grep ^Version

    Create requirements-dev.txt with that version:

    pre-commit==X.Y.Z

    Replace X.Y.Z with the version you just saw. Developer tools live here, never in requirements.txt, so they stay out of the application image you build in module 5.

  2. Create .pre-commit-config.yaml:

    repos:
      - repo: https://github.com/pre-commit/pre-commit-hooks
        rev: v0.0.0 # replaced by the next command
        hooks:
          - id: check-yaml
          - id: check-merge-conflict
          - id: check-added-large-files
          - id: end-of-file-fixer
          - id: trailing-whitespace
  3. Pin rev to the current release and install the hooks into this clone:

    pre-commit autoupdate
    pre-commit install
    pre-commit run --all-files

    autoupdate replaces v0.0.0 with the latest tag. Check that rev now shows a real version. The first run may fix whitespace in your files; that is expected.

Step 4: add the first workflow

  1. Create .github/workflows/pipeline.yml:

    name: pipeline
    
    on:
      push:
        branches: [main]
      pull_request:
    
    # Every job gets read-only access to the code unless it asks for more.
    permissions:
      contents: read
    
    jobs:
      smoke:
        runs-on: ubuntu-latest
        timeout-minutes: 10
        steps:
          - uses: actions/checkout@v4
    
          - uses: actions/setup-python@v5
            with:
              python-version: "3.12"
    
          - name: Install runtime dependencies
            run: pip install -r requirements.txt
    
          - name: Start the service and check it answers
            run: |
              python app.py &
              for i in $(seq 1 20); do
                curl -fsS http://127.0.0.1:8000/health && exit 0
                sleep 1
              done
              echo "service did not become healthy" >&2
              exit 1

    What each choice is for:

    • permissions: contents: read is the least privilege a checkout needs. Once a permissions block is present, everything it does not list is set to none.
    • timeout-minutes stops a hung or abused job early rather than letting it run to the platform’s much longer default.
    • The job installs only requirements.txt, proving the service runs with its runtime dependencies alone.
    • @v4 and @v5 are tags their owners can move. You pin them to full commit SHAs in the module 3 lab.
  2. Commit and push everything:

    git add .
    git status --short   # check: no .venv, no .env
    git commit -m "Add the Flask service, pre-commit and a smoke pipeline"
    git branch -M main
    git push -u origin main
  3. Watch the run:

    gh run watch

    The smoke job should go green.

Step 5: check the token really is read-only

  1. Open the latest run’s log and expand Set up job. Under GITHUB_TOKEN Permissions you should see Contents: read and nothing else.

    Or from the terminal:

    run_id=$(gh run list --limit 1 --json databaseId --jq '.[0].databaseId')
    gh run view "$run_id" --log | grep -A3 "GITHUB_TOKEN Permissions"
  2. Look at the repository’s default too: Settings → Actions → General → Workflow permissions. Set it to Read repository contents and packages permissions so a workflow that forgets its permissions block still gets the safe default.

Check your work

  • devsecops-lab is public and contains app.py, requirements.txt, requirements-dev.txt, .gitignore, .pre-commit-config.yaml and .github/workflows/pipeline.yml
  • requirements.txt lists Flask and its dependencies only, with exact versions, and no pre-commit
  • A .env file is ignored by Git
  • pre-commit run --all-files passes
  • The smoke job is green, with a 10-minute timeout and a read-only token
  • The repository’s default workflow permission is read-only

Keep the repository. The module 2 lab adds the secret scanner and protects main.

Key takeaways

  • Pinned versions in requirements.txt mean the scanners in later modules check exactly what you run.
  • Developer tools go in requirements-dev.txt so they never end up in the application image.
  • A .gitignore that lists .env stops the most common accidental secret commit before it happens.
  • Declare permissions: contents: read at the top of every workflow, and set a timeout on every job.
  • A green smoke job is your baseline: every security job you add later sits beside it.

Lesson questions

Answer all 10, then check them. You need 7 right to pass this lab and unlock the next one. Try as often as you like; after three tries the answers and explanations are shown. These are our own questions, written for this course.

  1. Why does the lab write requirements.txt with pip freeze right after installing Flask, before installing pre-commit?
  2. The workflow sets permissions: contents: read at the top. A later job needs to publish a package. What should you do?
  3. Why does the smoke job set timeout-minutes?
  4. A teammate runs git add . and a file called .env containing real database credentials is not staged. What prevented it?
  5. Why is the lab repository public?
  6. A workflow declares 'permissions: contents: read' and nothing else. What does the job's token get for permissions it does not list?
  7. What does the line '!.env.example' in the lab's .gitignore do?
  8. After creating .pre-commit-config.yaml with rev: v0.0.0, which command replaces that placeholder with a real release tag?
  9. The smoke job installs only requirements.txt before starting the service. What does a green run prove?
  10. A teammate adds a new workflow and forgets its permissions block. Which repository setting from the lab keeps its token safe?

Courses are taught against the frameworks they name. That is alignment, not certification: no course is certified, approved or endorsed by NIST, the DoD, DISA, FedRAMP, the CMMC program or any other agency or standards body, and completing one does not make a system compliant.